Architect
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.
- Add any file to your template repository and add Handlebars expressions to it. (docs)
- Add an
.architect.json
configuration file with your questions (docs) - Download the
architect
executable for your platform from the latest Release. - 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, overridesnonTemplates
)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.