Architect

License .github/workflows/rust.yml codecov Latest Release

Architect is a straightforward and technology-agnostic project scaffolding tool.

This means you can prepare templates for projects using any technology and Architect will spit out a perfect new project.

For its templating Architect uses the Handlebars templating language. Please check the documentation for more information.

>> 📚 Documentation <<

TL;DR

Architect uses Handlebars and Git to create proper projects from template repositories.

  1. Add any file to your template repository and add Handlebars expressions to it. (docs)
  2. Add an .architect.json configuration file with your questions (docs)
  3. Download the architect executable for your platform from the latest Release.
  4. Execute architect <PATH-TO-REPO> in your desired local directory, answer user-defined questions, et voila, you got a fully functional project created from a template. (docs)

Sample .architect.json

{
  "name": "My Awesome Microservice Template",
  "version": "1.0",
  "questions": [
    {
      "name": "author.name",
      "type": "Text",
      "pretty": "What's your name?"
    },
    {
      "name": "author.email",
      "type": "Text",
      "pretty": "What's your email address?"
    },
    {
      "name": "project.package",
      "type": "Identifier",
      "pretty": "What should be the root package for your Kotlin sources?",
      "default": "com.github.example"
    },
    {
      "name": "project.features",
      "type": "Selection",
      "pretty": "Which features would you like to use?",
      "items": [
        "jdbc",
        "kafka",
        "redis",
        "mySpecialLibrary"
      ],
      "default": [
        "jdbc",
        "mySpecialLibrary"
      ],
      "multi": true
    },
    {
      "name": "project.tests",
      "type": "Option",
      "pretty": "Do you want to generate test stubs?"
    },
    {
      "name": "current.time",
      "type": "Custom",
      "format": "^\\d\\d:\\d\\d$",
      "pretty": "What's the current time?"
    }
  ],
  "filters": {
    "conditionalFiles": [
      {
        "condition": "project.features.mySpecialLibrary",
        "matcher": "libs/mySpecialLibrary-*.jar"
      }
    ],
    "includeHidden": [
      ".github/**",
      ".gitignore"
    ],
    "nonTemplates": [
      "**/*.{gradle.kts,jar}"
    ]
  }
}

License and Contributions

Architect is provided under the terms of the BSD 3-Clause License.

Contributions are welcome, but you must have all rights to the contributed material and agree to provide it under the terms of the aforementioned BSD 3-Clause License.

A good idea would be to check out the Architect 1.0 project, it tracks all the work needed to get Architect to 1.0.

Installation

Prerequisites

Architect uses your existing Git installation, so you should have the git command on your PATH.

Using pre-built binaries

You can easily get pre-built binaries for Architect if you don't want to build it yourself.

Simply navigate to the Releases page on GitHub and download the latest binary for your platform.

Currently, the supported platforms are:

  • Linux (x86_64, libc)
  • Windows (x86_64)
  • macOS (darwin)

After downloading the executable file simply place it or a link to it in your PATH and you're ready to go.

Don't forget to mark the file as executable if you're on Linux or macOS:

chmod +x architect

Building from source

If you're in the mood for some action, or you want to try the latest features before they appear in a release you can get the source from GitHub and build Architect yourself.

Fork it or get the source from the repository, and you're ready to go.

Make sure you've got at least Rust 2021 installed and the cargo command is available.

Building

cargo build --bin architect

This command should create an executable file in the target/debug directory, ready for use.

To create an optimized build add the --release flag to the command:

cargo build --release --bin architect

The executable file should then be located in the target/release directory.

Testing

cargo test --bin architect

CLI

Architect provides a simple command-line interface that uses sensible default values to get going as quickly as possible.

To instantiate a template in a child directory of the working directory execute this command:

architect <PATH-OR-URL>

Architect follows the Git way of determining the output directory. If you don't explicitly specify an output directory, it will use the name of the source Git repository or the directory name of the path as the target directory name.

Example:

  • Git repository: https://github.com/v47-io/architect-test-template.git
  • Output directory: ./architect-test-template

Additionally, you can also specify a target directory yourself, e.g. if you don't want to use the name of the source repository:

architect <PATH-OR-URL> some-directory

Architect behaves the same as Git in this instance, it will create a directory with this name, relative to the working directory. Of course, you can also specify an absolute path.

By default, Architect will copy the entire Git history of the source repository to the target, or initialize the target as a Git repository. To prevent you from accidentally overwriting the template Architect removes the original Git remotes from the target.

Options

Architect offers some options to customize the behavior of Architect.

--b, --branch <branch>

Specify a different remote branch to fetch instead of the default branch of the repository.

Flags

To customize the behavior of Architect even further you can specify one or more flags as described here.

--local-git

Use your local Git installation instead of embedded Git.

This is intended as an escape hatch if you are using authentication scenarios not supported by the embedded Git functionality in Architect.

Architect is able to use most SSH agent scenarios and username/password authentication, so this should not be needed often.

--dry-run

Produces the same terminal output as normal operation without performing it.

This allows you to inspect the log output to determine whether Architect would perform its operations as intended.

Architect still takes all your input into account, it just stops shy of actually rendering and copying files to the target directory.

--dirty

Use the template source in its current (dirty) state.

This only has an effect if a local path is specified as the source repository. In that case Architect won't perform a Git clone but will just copy the directory, regardless of the local state.

This is most useful to test a template locally while you are developing it.

This option has no effect with remote repositories.

--ignore-checks

Ignore some failed checks that would prevent Architect from creating the target files.

These errors will be ignored:

  • Unexpected type of default value (for any question type)
  • Default value not matching the format (for custom questions)
  • Unknown default item (for selection questions)
  • Condition evaluation errors (for conditional files)

--no-history

Don't copy the Git history from the source repository to the target.

Instead, the target will be initialized as a new Git repository.

--no-init

Don't initialize the target directory as a Git repository.

This requires the --no-history flag to be present as well.

--verbose

Enables verbose output.

This is very technical at places. Make sure to specify this option before reporting a bug.

Templates

Architect makes it easy to scaffold entire projects from Git repositories, here referred to as templates.

All Git repositories are potential templates, and all files in them are also treated as Handlebars templates if they contain Handlebars template expressions indicated by {{ and }}. All other files are simply copied as long as they are not excluded.

On top of that all file names are also potential Handlebars templates that are rendered when generating sources from a template.

Architect preserves the original directory structure but allows the creation of new nested directory structures by way of Handlebars expressions in directory names.

Template Repositories

Instead of just using the entire Git repository as a template, Architect provides the option to maintain multiple templates in one repository and referring to them by their directory name when generating a project.

When using template repositories, each template must contain an .architect.json file to be usable as a template.

Looking at the following example you can then specify the option --template template-1 to use the specified template instead of the repository.

Example for a directory structure with multiple templates:

repository-dir
├── template-1
|  └── .architect.json
├── template-2
|  └── .architect.json
└── README.md

You can still use the entire repository as a template (by not specifying a template name), but all subdirectories containing an .architect.json file will be ignored.

Although nesting templates is not possible, you can still group multiple templates in directories.

To use one of the grouped templates of the following example you would specify the option --template template-group-1/template-1.

repository-dir
├── template-group-1
|  ├── template-1
|  |  └── .architect.json
|  └── template-2
|     └── .architect.json
├── template-3
|  └── .architect.json
└── README.md

Fetching

Architect uses Git to fetch remote repositories.

Fetching a remote repository is pretty straightforward, but Architect also supports fetching templates from a local directory.

Architect will either fetch it using Git, using the directory like a remote repository, or copy the directories contents, e.g. if the directory doesn't contain a Git repository.

When fetching from a local directory you can force Architect to copy the contents instead of using Git using the --dirty CLI flag. This is particularly useful when you want to test a template you are creating without having to commit your changes or pushing it to a remote Git repository.

Embedded Git and Fallback

Architect embeds some Git functionality to have portable way of fetching templates without depending on a local Git installation.

However, some authentication scenarios or other features provided by a proper Git installation are going to be absent, therefore breaking the functionality of Architect.

To circumvent this Architect offers the CLI flag --local-git which tells Architect to use the local Git installation instead of the embedded Git functions.

Structure

Architect treats the entire directory and all included files in a Git repository as templates.

This means that the original directory structure is preserved, and you can create additional nested directory structures when using Handlebars expressions.

Because all directory and file names are treated as potential Handlebars templates themselves, you can create nested directory structures, e.g. for Java packages.

Example:

Provided you have a directory in your template src/main/java/{{ package javaPackage }} there are few things to consider.

Let's consider javaPackage contains the value com.github.example. The package helper (described here) will transform that value to com/github/example. The resulting value will then be inserted into the path which will give us the final path src/main/java/com/github/example.

Architect will then create that path's nested directories copy all files from the source directory into this new, dynamically created directory.

.architect.json

The template directory can also contain a .architect.json file which can specify various configuration values influencing template rendering.

Configuration

You can configure a template by providing a .architect.json file.

The format of its content is described here using TypeScript, the root object is modelled as the Config interface.

/**
 * The configuration used by Architect when creating an instance of this project template.
 *
 * Everything (including the file itself) is optional, but Architect makes more sense to
 * use when actually configured
 */
export interface Config {
    /**
     * The name of the template.
     *
     * Can be used in handlebars templates using `__template__.name`
     */
    name?: string;
    /**
     * The version of the template.
     *
     * Can be used in handlebars templates using `__template__.version`
     */
    version?: string;
    /**
     * Questions to ask the user to specify dynamic context values.
     *
     * These values are then available in handlebars templates
     */
    questions?: Question[];
    /**
     * Contains multiple filters to control which files are actually considered and rendered
     */
    filters?: Filters;
}

export type Question = SimpleQuestion | SelectionQuestion | CustomQuestion;

export interface SimpleQuestion extends BaseQuestion {
    type: QuestionType.Identifier | QuestionType.Option | QuestionType.Text;
}

export interface SelectionQuestion extends BaseQuestion {
    type: QuestionType.Selection;

    /**
     * The items available for selection.
     *
     * These will be set to `true` in the context if selected.
     *
     * Format: `^[a-zA-Z_$][a-zA-Z0-9_$]*$`
     */
    items: string[];
    /**
     * Specifies whether multiple items can be selected
     */
    multi?: boolean;
}

export interface CustomQuestion extends BaseQuestion {
    type: QuestionType.Custom;

    /**
     * The regular expression that is used to validate the input for this question.
     *
     * When specifying a default value it must match this regular expression
     */
    format: string;
}

/**
 * This interface specifies the configuration properties that decide which files are considered
 * for Handlebars rendering or even included in the target directory
 */
export interface Filters {
    /**
     * Specifies conditions for certain files to be created.
     *
     * These conditions have full access to the context that is created by the questions.
     *
     * Note that conditions specified here don't apply to hidden files that weren't explicitly
     * included using `includeHidden` or files excluded using `exclude`
     */
    conditionalFiles?: ConditionalFiles[];
    /**
     * Specifies Glob expressions to include hidden files in the target.
     *
     * Note that including the `.git` directory here will have no effect
     */
    includeHidden?: string[];
    /**
     * Specifies Glob expressions to exclude files in the target.
     *
     * Note that exclusions have a higher precedence than inclusions and conditional files
     */
    exclude?: string[];
    /**
     * Specifies Glob expressions that indicate the files that should be rendered using Handlebars.
     *
     * This disables Handlebars rendering for all other files. Directory or file names are not affected
     */
    templates?: string[];
    /**
     * Specifies Glob expressions that indicate files that should not be rendered using Handlebars.
     *
     * This property has no effect, if `templates` is also specified
     */
    nonTemplates?: string[];
}

export interface ConditionalFiles {
    /**
     * The condition that decides whether the matched files are created.
     *
     * This is an expression that is handled by handlebars.
     *
     * The expression is automatically wrapped in curly braces (`{{` `}}`) so you
     * only need to specify the actual content of the expression here
     */
    condition: string;
    /**
     * A Glob string specifying the files affected by the condition
     */
    matcher: string;
}

interface BaseQuestion {
    /**
     * The name in the context for the value specified when answering this question.
     *
     * Can be multiple names concatenated using `.` to create hierarchical structures in
     * the context.
     *
     * Format: `^[a-zA-Z_$][a-zA-Z0-9_$]*$`
     */
    name: string;
    /**
     * The type of the question, which indicates the expected values
     */
    type: QuestionType;
    /**
     * A properly spelled out question to ask instead of just presenting the name when
     * processing input
     */
    pretty?: string;
    /**
     * The default answer for this question.
     *
     * If the question is of type `Option`, this should specify a boolean, if it's 'Selection'
     * you can specify either a string or a list of strings, otherwise just a string.
     *
     * Note: Specifying a list of strings will only be accepted if the `Selection` question
     * allows the selection of multiple items
     */
    default?: string | boolean | string[]
}

export enum QuestionType {
    Identifier = 'Identifier',
    Option = 'Option',
    Selection = 'Selection',
    Text = 'Text',
    Custom = 'Custom'
}

Context

Architect builds a Handlebars context using some provided information and the values defined when answering configured questions.

This context is available to all instances where Handlebars templates are processed, so you can access specified values in conditions, or file/directory names.

Context consists of multiple nested objects and properties which contain values.

Default Context

By default, Architect provides the following data in the context, provided values are configured:

{
  "__template__": {
    "name": "The template name from in .architect.json or undefined",
    "version": "The template version from .architect.json or undefined"
  }
}

You cannot add to the __template__ object using your questions, and Architect will reject any attempt to do so. However, you can still define a __template__ object in any nested object further down the line.

Something like this would be possible:

{
  "__template__": ...,
  "yourOwnObject": {
    "__template__": {
      "yourProperty": "some data"
    }
  }
}

File Context

In addition to the default context Architect adds some information about the current template file to the context:

{
  "__template__": {
    ...,
    "file": {
      "rootDir": "the working directory, not the target directory",
      "sourceName": "the original file name of the template file",
      "sourcePath": "the original path of the template file",
      "targetName": "the final file name of the rendered template file",
      "targetPath": "the final path of the rendered template file"
    }
  }
}

Questions

In the template configuration you can define questions that Architect should ask before the files can be written to the target. The answers to those questions are stored in the context and are therefore available in the context.

You can ask various types of questions, each with a concrete use-case.

The question names must be (possibly dot-delimited) identifiers, so that they can be used in Handlebars templates. By specifying multiple dot-delimited identifiers you can create nested objects in the context.

Multiple occurrences of previously specified question names will overwrite prior ones without warning.

Example:

You define questions with the following names:

  • author.name
  • author.email
  • somethingElse

Result in the context:

{
  ...,
  "author": {
    "name": ...,
    "email": ...
  },
  "somethingElse": ...
}

Default Values

You can also specify default values for all your questions. Specifying a default value makes the question optional and you can proceed without entering a custom value.

Identifier

Ask for an identifier, i.e. a String that can only consist of a limited subset of characters, or multiple such strings concatenated by dot (.) characters.

Format:

^[a-zA-Z_$][a-zA-Z0-9_$]*$

A possible use-case for this question type can be to ask for a class name for an application's main class or a package name for Java.

Default Values

You can specify any value as a default value that would fit the specified format.

Option

A simple yes or no question. This will store a Boolean in the context.

Example:

You specify yes for the question insertLogStatements.

Result in the context:

{
  ...,
  "insertLogStatements": true
}

This question type is useful when you want to present the user with a binary choice, e.g. whether to generate a .gitignore in the target directory.

Default Values

You can specify a boolean value as the default value (true or false)

Selection

Useful where you want to offer a predefined list of values to choose from. The selected value(s) will be created as properties in the context and have the value true.

You can configure a selection question to accept only one or multiple (multi) values to be selected.

As those values will be added as properties they must match a certain format:

^[a-zA-Z_$][a-zA-Z0-9_$]*$

Example:

The question (whichOnes) defines three values: value1, value2, and value3. You select value1 and value2.

Result in the context:

{
  ...,
  "whichOnes": {
    "value1": true,
    "value2": true
  }
}

A possible use-case for this type of question is to define several features a user could choose to enable when using a template, e.g. whether to use logging statements, or to include a certain dependency.

Default Values

Depending on whether multi-selection is enabled you can either specify a single string as the default value or an array of strings.

Text

This question type allows you to ask for arbitrary text to store in the context. No format is enforced for answers.

Possible use-cases for this question type can be to ask for a person's full name or an email address.

Default Values

You can set any string as the default value.

Custom

You can define a custom regular expression to validate the input. Please keep in mind that the regular expression will look for the shortest matching string, so you must specify anchors (^, $) if you want to match the entire input.

Architect uses this implementation.

Default Values

You can set any string that matches the Regular expression as the default value.

Filters

The Architect configuration file allows you to define filters in multiple ways to control whether certain files get actually added to the target.

The filters are:

  • exclude (Exclusion of files, strongest)
  • includeHidden (Inclusion of certain hidden files)
  • conditionalFiles (Inclusion of files if a certain condition is true)
  • templates (Which files to treat as templates and render, overrides nonTemplates)
  • nonTemplates (Which files not to render as templates)

These filters have a precedence assigned to them, exclusions are the strongest. Files that match an exclusion rule are never added to the target. Hidden files can only be added through a condition if they have also been matched by an explicit includeHidden expression. If you include a hidden file, but a condition that matches it is false, it's not added to the target.

Keep in mind that the top-level .git directory cannot be matched by any of those filters and is always excluded from explicit processing. Architect handles that .git directory separately.

All filters use glob expressions that match the entire relative path to the root directory of the template.

Please see this for more information on the supported syntax for glob expressions (Architect enables the literal_separator option and case-insensitive matching).

Exclusions

Define glob expressions to match files you don't want to have in the target.

Include Hidden

Architect by default excludes all hidden files or directories. This only applies to files or directories starting with a dot (.) character, files marked as hidden in Windows will still be included.

Define glob expressions to match hidden files you want to include anyway.

Please keep in mind that you cannot include the top-level .git directory or any of its descendants using this.

Conditional Files

Architect will only include matched files if the specified condition returns a "truthy" result. Architect will evaluate all matching conditions in sequence until one returns a "truthy" value.

Here you define glob expressions to match files, and Handlebars expressions to determine whether these files should be included. These Handlebars expression don't need to be delimited by {{ and }}, and have full access to the context.

The glob expression will be applied to the file in the source repository, so before any possible Handlebars templates in file or directory names are evaluated.

Please keep in mind that when working with hidden files conditions can only be applied to files included using includeHidden.

Format in the configuration file:

// Config
    /**
     * Contains multiple filters to control which files are actually considered and rendered
     */
    filters?: Filters;

// Filters
    /**
     * Specifies conditions for certain files to be created.
     *
     * These conditions have full access to the context that is created by the questions.
     *
     * Note that conditions specified here don't apply to hidden files that weren't explicitly
     * included using `includeHidden` or files excluded using `exclude`
     */
    conditionalFiles?: ConditionalFiles[];

// ConditionalFiles
    /**
     * The condition that decides whether the matched files are created.
     *
     * This is an expression that is handled by handlebars.
     *
     * The expression is automatically wrapped in curly braces (`{{` `}}`) so you
     * only need to specify the actual content of the expression here
     */
    condition: string;
    /**
     * A Glob string specifying the files affected by the condition
     */
    matcher: string;

Templates

Define glob expressions to match files you want to have rendered as templates, this can be seen as an allow-list or whitelist that defines rendered files.

This leads to all other files not being rendered as Handlebars templates, instead they are copied as-is.

templates overrides nonTemplates, so even if you specify the latter it won't have any effect.

Non-Templates

Define glob expressions to match files you don't want to have rendered as templates, this can be seen as a deny-list or blacklist that defines non-rendered files.

Files that are matched by this are copied as-is instead of being rendered using Handlebars.

This property is ignored if templates is configured.

Rendering

Architect renders files and names using Handlebars and treats all files as potential Handlebars templates.

To find out whether a file actually is a template Architect looks at its contents to look for the "mustaches" ({{ and }}). Should Architect find both in that order on the same line it will treat the file as a template.

File and directory names are also potential templates that are rendered using Handlebars if they contain the mustaches.

Helpers

Architect provides a suite of Handlebars helpers to make writing your templates a breeze.

By default, Architect provides all helpers supported by the handlebars and the handlebars_misc_helpers libraries.

Rhai scripting in templates (as provided by the handlebars library) is not supported!

Please take a look at the handlebarsjs Language Guide for guidance on how to actually use helpers.

package

In addition to all those helpers Architect provides a helper off its own: package. This helper is intended to help you write better templated directory names.

You can use this helper to create nested directory structures for your files, its use is to create multiple, nested directories from dot-separated values, e.g. a Java package.

This helper is not available in template files, only file or directory names.

Example:

  • File path: src/main/java/{{ package javaPackage }}/Main.java
  • Context: "javaPackage": "com.github.example"
  • Result: src/main/java/com/github/example/Main.java

Expert Mode

This section contains information about topics that are not important for the everyday user, but you might find them interesting if you're looking into tweaking the internals of Architect or want to know more about how it does its thing.

Environment Variables

Architect offers you the ability to modify some behaviors through the use of environment variables.

RENDER_PARALLELISM

By default, Architect uses multiple threads to render the templates.

This is the formula: max(1, min(4, number_of_threads / 2))

number_of_threads is the number of threads available on your machine.

If you think four threads are too little or too much, you can change this to any other integer > 0.

TEMPLATE_INSPECT_MAX_LINES

Architect won't read entire files to find out if they are templates. By default, it'll only read 25 lines of each file to find out.

If you have files where the Handlebars "mustaches" only start occurring after the 25th line, this environment variable is for you. Just set it to any other integer > 0.

LINE_BUFFER_CAPACITY

Architect uses a buffer to read lines into memory one at a time. By default, this buffer is 256 bytes long.

Because the buffer expands automatically this isn't even something I'd recommend adjusting, but if you really want to, set it to any other integer > 0.