Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/reddec/layout
Generate new project from layout. Supports typed user-input, hooks, and conditions.
https://github.com/reddec/layout
cookiecutter layout project-bootstrapping project-generator
Last synced: about 1 month ago
JSON representation
Generate new project from layout. Supports typed user-input, hooks, and conditions.
- Host: GitHub
- URL: https://github.com/reddec/layout
- Owner: reddec
- License: apache-2.0
- Created: 2022-05-03T08:57:27.000Z (over 2 years ago)
- Default Branch: master
- Last Pushed: 2024-08-26T02:00:06.000Z (4 months ago)
- Last Synced: 2024-08-26T03:21:03.254Z (4 months ago)
- Topics: cookiecutter, layout, project-bootstrapping, project-generator
- Language: Go
- Homepage:
- Size: 159 KB
- Stars: 30
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Layout
Generate new project from layout. Supports typed user-input, hooks, and conditions.
Inspired by [cookiecutter](https://github.com/cookiecutter/cookiecutter), [yeoman](https://yeoman.io), and Ansible.
You may think about it as cookicutter-ng or modern cookiecutter.
## Very quick demo
`layout new reddec/layout-example my-example`
Will ask you questions and generate hello-world HTML page based on your answers.
[![asciicast](https://user-images.githubusercontent.com/6597086/166427829-320fefc2-7131-46a3-85e8-655f5069c2a2.gif)](https://asciinema.org/a/zDPT7o2sbxOjPHoNd5z0qnxvG)
## Installation
- **Pre-build binary**: prepared for most OS in [releases](https://github.com/reddec/layout/releases).
- **DEB/RPM/APK packages**: in [releases](https://github.com/reddec/layout/releases).
- **From source**: requires Go 1.18+, `go install github.com/reddec/layout/cmd/...@latest`
- **Brew**: `brew install reddec/tap/layout`
- **Docker**: (supports amd64 and arm64)
- `ghcr.io/reddec/layout:latest`
- `ghcr.io/reddec/layout:`Note 1: Docker image built from `scratch` and not very useful by itself. Intended to be used as part of multi-stage
Docker build.Note 2: During cloning from GitHub you may observe SSH-key related error, which usually happens because of GitHub key
rotation. It could be fixed by `ssh-keyscan github.com >> ~/.ssh/known_hosts`## Motivation
Heavily inspired by [cookiecutter](https://github.com/cookiecutter/cookiecutter) and [yeoman](https://yeoman.io),
however layout offers additional features and bonuses:- single binary without runtime dependencies, compiled for all major OS
- supports boolean variables ([yikes, cookicutter!](https://github.com/cookiecutter/cookiecutter/issues/126))
- supports conditional
variables ([cookiecutter, I am again pointing to you](https://github.com/cookiecutter/cookiecutter/issues/1438))
- supports plain includes and conditional includes (inspired by Ansible)
- supports (and validates): string, boolean, list of strings, integer, float
- supports versioning in case you want to lock specific version of `layout`
- supports file source and remote Git repository (even without `git` installed!)
- supports multiple inline hooks (with portable shell) and templated hooks
- hooks also supports condition :-)
- supports normal labeling for variables input (cookiecuter...)
- supports multiple layout in one repo
- supports `.layout.yaml` overlay file in current directory for re-using organization-wide configuration per directoryI generally do not like competing with other open-source projects but this time
I would like to say that this project is aiming to fix legacy cookiecutter's problems
and keep the best of three worlds (including yeoman and Go).The utility is designed to be completely universal not just in terms of supported languages
and approaches, but also in terms of operational experience and can be used in restricted (limited to no-access to
internet) environment with the same convenience as in public.This project stands on open-source atlantis shoulders:
- [MVDan's protable shell](https://mvdan.cc/sh/) which allows writing inline shell script regardless of OS
- [Tengo language](https://github.com/d5/tengo) which provides complete, simple and fast language for conditions
- [Go-git](https://github.com/go-git/go-git) which is basically embedded go-native Git client
- [Survey](https://github.com/AlecAivazis/survey) provides fancy terminal UI
- [Masterminds](https://github.com/Masterminds) for supporting tools... and many many others. I love open-source, and this project is one of my little contributions.
That's why [license](LICENSE) for the project is Apache 2.0 which means that you may use code as you wish but please
state any changes (for legal details please read LICENSE file).## Documentation
- Use GitHub navigation on top of README, or use search-on-page functionality in your viewer
- Use `layout new --help` command to check usage### CLI usage
#### General
Usage:
layout [OPTIONS]
Create new project based on layout
Author: Aleksandr Baryshnikov
Help Options:
-h, --help Show this help message
Available commands:
new deploy layout
show show configuration#### show
Usage:
layout [OPTIONS] show
Help Options:
-h, --help Show this help message
Available commands:
config current config
config-file location of default config file#### new
Usage:
layout [OPTIONS] new [new-OPTIONS] [source] [destination]Help Options:
-h, --help Show this help message
[new command options]
--version= Override binary version to bypass manifest restriction [$LAYOUT_VERSION]
-c, --config= Path to configuration file, use show config command to locate default location [$LAYOUT_CONFIG]
-u, --ui=[nice|simple] UI mode (default: nice) [$LAYOUT_UI]
-d, --debug Enable debug mode [$LAYOUT_DEBUG]
-a, --ask-once Do not retry on wrong user input, good for automation [$LAYOUT_ASK_ONCE]
-D, --disable-cleanup Disable removing created dirs in case of failure [$LAYOUT_DISABLE_CLEANUP]
-g, --git=[auto|native|embedded] Git client (default: auto) [$LAYOUT_GIT]* `-g,--git` (v1.2.0+) specifies git client which should be used:
* `native` use native Git binary (must be 2.13+)
* `embedded` use Golang native git client (safe mode)
* `auto` (default, but it can be changed in [configuration](#configuration)) in case git installed (`git` binary
accessible) and git version is 2.13 or higher `native` will be used, otherwise `embedded`* (v1.4.0+) if `source` is not set, const of `.layout` file in the current dir will be used for URL
* (v1.4.0+) if `destination` is not set, the `default` URL from config will be setSince 1.4.0 it's possible to run just `layout new`.
##### set
Since v1.3.1
Usage:
layout [OPTIONS] set
Help Options:
-h, --help Show this help message
Available commands:
default URL pattern to resolve layout
git git client mode### Architecture
```mermaid
sequenceDiagram
User->>layout: new
layout->>repo: fetch recursively, depth 1
repo->>layout: data
layout->>User: display questions
layout->>destination: copy and render content, execute hooks
```Let's describe basic example.
Assume we made demo repository as layout which located in `https://github.com/reddec/layout-example`.Once you executes `layout new https://github.com/reddec/layout-example my-example`:
1. `layout` goes to server which hosts repository (`github.com`) by desired protocol (`https`) and asks for content of
repository `layout-example` owned by `reddec`.
2. (optionally) `layout` negotiates authorization protocols being aware of configuration in `.gitconfig`
3. `layout` makes shallow (depth 1) clone of repo to a temporary directory
4. `layout` reads `layout.yaml` and asks questions from user
5. `layout` creates destination directory (`my-example`) and copies data from `content` directory from cloned repo as-is
6. `layout` executes `before` hooks
7. `layout` renders file names and removes files and directories with empty names
8. `layout` renders content of files except marked as ignored in `ignore` section
9. `layout` executes `after` hooks
10. done> In reality, `layout` will first try to resolve URL as local directory, as abbreviation,
> and only at last it will decide go to remote URLBy default, for GitHub repositories host and protocol not needed. For example, instead
of `layout new https://github.com/reddec/layout-example my-example` we can
use `layout new reddec/layout-example my-example`.
See [configuration](#configuration) for details.### Layout structure
Once repository fetched, `layout` will scan directories with `layout.yaml` files. Each directory with such file will
be marked as project directory.In case there is only one project directory, then it will be automatically picked. Otherwise, user will be prompted to
pick desired project (based on `title` field).Each project directory should contain:
- `layout.yaml` - main manifest file
- `content` - content directory which will be copied to the destinationValid repo structure:
**one repo - one layout**:
```
/
├── layout.yaml
└── content
```**one repo - many layouts** (v1.3.0+):
```
/
├── foo
│ └── layoutA
│ ├── layout.yaml
│ └── content
└── layoutB
├── layout.yaml
└── content
```### Manifest
Check examples in:
- https://github.com.com/reddec/layout-example
- [test data](test-data) directory**absolute minimal example of manifest:**
```yaml
{ }
```Yes, empty object is valid manifest.
**'hello world' example of manifest:**
```yaml
prompts:
- var: name
after:
- run: "echo 'Hello, {{.name}}!' | wall"
```(*nix only, should broadcast message `Hello, !`)
#### Version
Layout manifest supports constraints of applied layout binary version based on [semver](github.com/Masterminds/semver).
In case `version` is not specified, all versions of `layout` are allowed.
For now, I suggest pinning major version only: `~1`. `layout` is following semantic version and all version withing
one major version are backward compatible (manifest designed for `1.0.0` will work normally even in `layout`
version `1.9.5`, but without guarantees for `2.0.0`).#### Title and Description
There are two informational field in manifest:
* `title` - short description about layout. Only title will be shown to user in case of selecting manifest in
multi-project repo
* `description` - full project descriptionBoth title and description will be shown before prompts.
#### Delimiters
Template delimiters could be overridden by in `delimiters` section. Defaults are `{{` (open), `}}` (close).
Could be useful in case you are trying to render Go templates.Example:
```yaml
# ...
delimiters:
open: '[['
close: ']]'
# ...
computed:
- var: foo
value: "[[.dirname]]"
# ...
```#### Prompts
Prompts are list of variables which should be asked from user.
Minimal required field is `var: ` which unique identifies provided value
and could be used in conditions and templates later.* `label` is used as a question which will shown to user. If not set - `var` name will be used as-is
* `type` variable type, default is `str`. If user input can not be casted to desired type, user will be asked again (
unless `-a/--ask-once` provided). Supported types:
* `str` - user input as-is (space trimmed)
* `int` - user input should be 10-base 64-bit integer
* `float` - user input should be 10-base 64-bit float
* `bool` - `true` if user input (case insensitive) is `t`, `y`, `yes`, `true`, or `ok`; otherwise `false`
* `list` - list of strings. If options are not provided, values are comma-separated
* `default` - default suggested value. Since v1.3.0 it can be an array, which is useful when you want to pick multiple
default values for `type: list`
* `options` - allows user select single option (not `type: list`) or multiple options (type: `list`)
* `when` - condition written in [tengo language](https://github.com/d5/tengo) which should return boolean;
default `true`. Variable will not be defined (use [defaults](#defaults) if needed) and prompt will not be rendered or
executed if condition returned false.Example:
```yaml
# ...
prompts:
# cast to integer
- var: age
label: What is your age
type: int
# conditional
- var: tin
label: What is your TIN
when: age > 18
# select one
- var: degree
label: What is your highest degree
options:
- Elementary school
- High-School
- Bachelor
- Magister
- PhD
# select many
- var: source_of_income
label: Source of income
type: list
options:
- employed
- self-employed
- business
# ...
```#### Includes
`include` is a special instruction within [`prompts`](#prompts) which loads separate yaml
file, relative to the current, with list of another prompts.Used as a convenient method to group questions blocks.
Example:
_layout.yaml_
```yaml
# ...
prompts:
- var: age
label: What is your age
type: int- include: adult.yaml
when: age > 18
# ...
```_adult.yaml_
```yaml
- var: tin
label: What is your TIN- var: degree
label: What is your highest degree
options:
- Elementary school
- High-School
- Bachelor
- Magister
- PhD- var: source_of_income
label: Source of income
type: list
options:
- employed
- self-employed
- business
```#### Computed
The `computed:` invoked after user input and can contain conditions.
Most often it could be useful for defining re-usable variable which depends on user-input. For example:```yaml
prompts:
- var: owner
- var: repo
computed:
- var: github_url
value: "https://github.com/{{.owner}}/{{.repo}}"
```**Note**: in case variable `value` is string, then content of the field will be rendered as template. Otherwise, it will
be used as-is.```yaml
prompts:
- var: owner
- var: repo
computed:
- var: options
value:
- 1234
- "option {{.repo}}" # <-- will be used as-is with brackets since value content is array, not string
```#### Defaults
The `default:` section is similar to `computed`, however, invoked before user input and can not contain conditions.
Most often it could be useful together with conditional include to prevent excluded variables be undefined in
expressions.Example:
_layout.yaml_
```yaml
prompts:
- var: ask_name
type: bool
- include: name.yaml
when: ask_name
after:
- run: echo Hello {{.name}}
when: name != ""
```_name.yaml_
```yaml
- var: name
```In case `ask_name` set to `false` the hook **will fail** because in hook condition `name != ""` used undefined variable.
To fix it, you may update manifest with defaults variables:
_layout.yaml_
```yaml
default:
- var: name
value: ""
prompts:
- var: ask_name
type: bool
- include: name.yaml
when: ask_name
after:
- run: echo Hello {{.name}}
when: name != ""
```Rules of rendering value in `default` section is the same as in [`computed`](#computed).
Defaults also can be defined globally in [configuration](#configuration). Optionally, to make layout portable you may
use template in default section._$XDG_CONFIG_HOME/layout/layout.yaml_
```yaml
values:
country: Global
```_layout.yaml_
```yaml
default:
- var: country
value: '{{with .country}}{{.}}{{else}}my-default-country{{end}}'
```#### Ignore
Ignore list allows you define list of [glob](https://pkg.go.dev/path/filepath#Glob) patterns of paths which should not
be rendered as template.Example:
```yaml
ignore:
- "**/*.css" # do not treat as template CSS files
```#### Hooks
Hooks can be defined through inline portable shell or through templated script.
* `before` hooks executed with resolved state (after user input and computed variables), before rendering paths and
content
* `after` hooks executed after content renderedOptionally, a `label` could be defined to show human-friendly text during execution.
Working directory for script and inline always inside destination directory. For script invocation, path to script is
relative to layout content.Example:
```yaml
#...
before:
# inline script
- label: Save current date
run: date > created.txt
after:
# file script
- label: Say hello
script: hooks/hello.sh "{{.dirname}}"
#...
```Content of `hooks/hello.sh` could be (`foo` should be defined):
```shell
#!/bin/shwall Hello "{{.foo}}" "$1"
```### Rendering
By-default, all files in `content` directory treated as [golang template](https://pkg.go.dev/text/template), unless some
paths added to [`ignore`](#ignore) section.All defined variables are accessible in a root context: `var: foo` is available as `{{.foo}}`
Additional "magic" vairables:
- `dirname` (usage: `{{.dirname}}`) - base name of destination directory, commonly used as project name
#### Functions
* [Sprig template utilities](http://masterminds.github.io/sprig/) available.
* Custom functions:
* `getRootFile` (v1.2.1+) - (`{{getRootFile "myfile"}}`) get content of file with specific name in any of root
folders. Example:Current working directory: /foo/bar/xyz
Looking for name: .gitignore
Will check (and return content):
/foo/bar/xyz/.gitignore
/foo/bar/.gitignore
/foo/.gitignore
/.gitignoreIf nothing found - `ErrNotExists` returned
* `findRootFile` (v1.3.2+) - (`{{findRootFile "myfile"}}`) find path to file with specific name in any of root
folders. Same as `getRootFile` but instead of returning content it is returning path to file.
* `findRootDir` (v1.3.2+) - (`{{findRootDir "mydir"}}`) find path to directory with specific name in any of root
folders. Same as `findRootFile` but instead of looking for file it is looking for directory.
* `findSubmatch` (v1.3.3+) - capture all regex groups with matching pattern. Example:Pattern: foo[ ]+([^ ]+)
Text: foo bar foo baz
Returns: [bar, baz]#### Flow
In manifest, the following items also renders just before usage:
- in [prompts](#prompts)
- label
- include
- default
- options
- in [computed](#computed)
- value (if string)
- in [defaults](#defaults)
- value (if string)
- in [hooks](#hooks)
- run
- scriptDirectories and file names can contain templates too. Ex: `content/src/{{.project}}/main-{{.foo}}.go`.
In case path segment rendered to an empty string, the segment will be removed (mimics cookiecutter behaviour).
### Condition expression
Statement written in [tengo language](https://github.com/d5/tengo) which should return boolean.
Can be used in:
- [prompts](#prompts) and [includes](#includes)
- [computed](#computed)
- [hooks](#hooks)Helpers:
- `has(seq, opt) -> bool` returns true if `seq` contains value `opt`. Mostly used for checking selected options (
type: `list`)Example:
```yaml
# ...
prompts:
- var: features
options:
- http
- ui
- js
type: list- include: http.yaml
when: 'has(features, "http")'
# ...
```### Configuration
The global configuration file defines user-wide settings such as:
* abbreviations
* default repository template
* global default variablesIf `--config, -c` not provided, the global configuration file will be used which is located
under `/layout/layout.yaml`.
You may check actual location by command `layout show config-file`.Specifically:
* On Unix systems, `$XDG_CONFIG_HOME/layout/layout.yaml` or `$HOME/.config` (if `$XDG_CONFIG_HOME` not set).
* On Darwin (Mac), `$HOME/Library/Application Support/layout/layout.yaml`
* On Windows, `%AppData%/layout/layout.yaml`
* On Plan 9, `$home/lib/layout/layout.yaml`Currently, it supports:
* `abbreviations`: map of string -> template values where key is repo shorthand and template is string with `{0}`
which will be replaced to the repo details. You may use abbreviations as `:/`
* `default`: template for repository without shorthand, default (if not set) is `[email protected]:{0}.git`.
* `values`: (v1.2.0+) map of anything where key as name and value is default value (any valid YAML type)
* `git`: (v1.3.1+) preferred git mode (same as in [cli](#new)): `auto` (default), `native`, `embedded`> Hint: you may use air-gap deployment in case you stored bare repository somewhere locally.
Since v1.4.0 it's possible to define overlay in the current directory `.layout.yaml`. Content from this file will
be merged on top of global config.Example:
```yaml
default: "[email protected]:{0}.git" # sets default repo to GitLab instead of GitHub. Could be used as some-owner/some-repo
abbreviations:
ex: "ssh://[email protected]/{0}.git" # could be used as ex:some-owner/some-repo
values:
author: RedDec
organization: myself
```Check [roadmap](#roadmap) for upcoming features.
## UI
Currently, layout provide two terminal UI:
- `nice` (default) - colored, interactive UI, backed by [Survey](https://github.com/AlecAivazis/survey)
- `simple` - plain STDIN/STDOUT UI, which can be useful for automation or for basic terminalsYou may pick UI kind for `new` [command](#new) by flag `--ui ` or `-u `.
For example, to use simple UI: `layout new -u simple reddec/layout-example my-example`.
## Automation
`layout` supports naive automation where input variables are fed through STDIN.
Use `layout new -u simple -a ` to simplify integration with another tools:- `-u simple` disables interactive [colorful UI](#ui)
- `-a` enables mode "ask once" for `new` [command](#new) which disables retry-loop in case of malformed user input## Security and privacy
**Privacy**: we (authors of layout) do not collect, process or transmit anything related to your activities to our or
third-party servers with one exception.
Exception is the moment when you are cloning remote repository: we are not responsible for data leakage or tracking
activities from repo owner. We are using standard git protocol (via [go-git](https://github.com/go-git/go-git)) which
requires some "trust" to remote repository, however, this warning is not specific to only `layout`. Just be careful what
and from where you are cloning (see below).**Security** is a bit bigger problem due to nature of idea behind the `layout`: hooks defined in manifest could
potentially do anything in computer limited by the running user permissions. There is no universal solutions for the
problem, however:- (suggested) clone only from trusted repo
- (paranoid) execute layout in minimal sandbox environment such as docker or kvm and copy result data to the host.Anyway: running `layout` under `root` privileges is a TERRIBLE IDEA, you should never do it.
See [roadmap](#roadmap) for planning related features.
## Roadmap
- Security
- clone by commit digest
- disable hooks during cloning, however, it may break all idea of `layout`
- UX
- global before/after hooks
- globally disable hooks
- compute variables by script
- support `.layout` file in working path
- support `include` in before/after hooks
- Delivery
- apt repository
- Arch AUR
- Long term ideas
- GUI for prompts (maybe)
- Decentralized marketplaces/discovery repositories## Contributing
PR/Issues/Feedback always welcome.
### Rules
- Be professional and avoid personal conflicts. I strongly believe that FOSS still can be a cross-broder collaboration
regardless of politics (at least we can try).
- No CLA (contributor license agreement), no corporate bullshit - once your code is merged it's available for everyone
under Apache 2.0 license
- Do not forget to add or update Apache 2.0 header in code!
- No one is perfect, if you have an idea/draft of code - make draft PR and let's talk about how to improve it. If you
can -
help others to complete their ideas
- Do not demand from volunteers immediate reaction. If you want to prioritize your problem - motivate them (for example:
contact me directly) by donation or by any other form### Flow
- Standard GitHub flow:
- fork
- make PR (draft PRs are not reviewed)
- ask for review -> fix problems -> ask review (loop)
- done! (PR merged)Do not hesitate to ping maintainers again and again. Maintainers are busy and your review request may sink under
others problems.