β
βββ³ββββββ repo
βββββ£βββ templating π
β
Create projects from interactive templates, or enrich existing projects using interactive recipes.
Any public repository can be a template for your next project. tmplr
will download it (without git history) and execute its templating recipe, if one exists,
interactively filling up the project with contextual information.
npx tmplr owner/repo # π get repo from github
npx tmplr gitlab:user/repo # π₯½ or gitlab
npx tmplr [email protected]:user/repo # πͺ£ or bitbucket
npx tmplr https://git.sr.ht/user/repo # π or source hut
npx tmplr local:/some/template # π or local template
Recipes set tmplr
apart from other scaffolding tools:
- π± They can do simple tasks like removing a license file, updating README using git info, etc.
- πΈ They can do complex tasks such as adding new packages to a monorepo from a chosen preset.
- π§ They can use context, such as git info or directory name, to fill the template.
- π¬ They can interactively ask for more info if needed.
- 𧩠They can use other templates and reusable recipes.
- βοΈ They are powerful yet safe to run on your machine.
- π° They are super easy to write and understand.
Install Node.js and npm, then run with npx:
npx tmplr owner/repo
πΊ You can install tmplr
globally too:
npm i -g tmplr
tmplr owner/repo
π Use @latest
tag to install/run the latest version:
npx tmplr@latest owner/repo
npm i -g tmplr@latest
Use version
command to check for updates:
npx tmplr version
Get public repositories from GitHub:
npx tmplr owner/repo
For example, use this template to create a publishable React component:
npx tmplr vitrin-app/react-component-template
Get public repositories from GitLab, BitBucket or SourceHut:
# π₯½ download from GitLab
tmplr gitlab:owner/repo
tmplr [email protected]:owner/repo
tmplr https://gitlab.com/owner/repo
tmplr gitlab:owner/group/repo --subgroup
# πͺ£ download from BitBucket
tmplr bitbucket:owner/repo
tmplr [email protected]:owner/repo
tmplr https://bitbucket.org/owner/repo
# π download from Sourcehut
tmplr git.sr.ht/owner/repo
tpmlr [email protected]:owner/repo
tpmlr https://git.sr.ht/owner/repo
# π use local template
tmplr local:/path/to/template
Get a tag, branch, commit or subdirectory:
tmplr owner/repo#branch # π branch
tmplr owner/repo#tag # π release tag
tmplr owner/repo#c0m1th45h # π commit hash
tmplr owner/repo/subdirectory # π sub directory
π Read more about command line options here.
Tip
For cloning gitlab subgroups, use the --subgroup
flag:
tmplr gitlab:dude/fun-projects/starter-recipe --subgroup
If you have a repo with a recipe file locally and just want to run the recipe,
go to the project directory and run tmplr
without arguments:
npx tmplr
Tip
A recipe is a .tmplr.yml
file that modifies the project via interactive prompts / contextual info.
Reusable recipes only change a part of your project, instead of determining its whole shape. For example, this reusable recipe helps you choose a license for your project. Use it like this:
npx tmplr use trcps/license
While you can use only one template for your project, you can use multiple reusable recipes. For example, add a license, and then use this recipe to add a GitHub action for automatic publishing to NPM:
npx tmplr use trcps/license
npx tmplr use trcps/npm-autopublish
Tip
tmplr use
accepts same arguments for using a template:
tmplr use owner/repo#mit # π get a branch
tmplr use owner/repo#v1.0.0 # π get a tag
tmplr use bitbucket:owner/repo # πͺ£ get from bitbucket
tmplr use local:/path/to/repo # π get from local
π Read more about the
use command
here.
Use --dir
(or -d
) option to change the working directory (default is .
):
# π will clone owner/some-repo into my-new-project
tmplr --dir my-new-project owner/some-repo
# π will run the recipe some-project/.tmplr.yml
tmplr -d some-project
Caution
Recipes can change files only inside the working directory. By choosing their working directory, you basically choose which files they will have access to.
Generally, you should not run arbitrary scripts from untrusted sources on your machine. tmplr
recipes are limited in what they can do, so that they can't do malicious acts, while remaining powerful enough for any scaffolding task.
- The scope of recipes is limited to the working directory:
- Recipes can read, write, and remove files in their scope.
- Recipes can download contents of public repositories, from trusted sources (GitHub, GitLab, BitBucket & SourceHut), to their scope.
- Recipes can read some contextual values.
- Recipes can read environment variables.
Every public repository is a template. They can become more convenient to use by adding a recipe to interactively fill up the project using user's context. Simply add a .tmplr.yml
, located at the root of your repo. People can use your template by running this:
npx tmplr your/repo
Use preview
to test how your repo would act as a template:
npx tmplr preview
π Read this to learn more about previewing templates.
A recipe instructs tmplr
on how to update project files with contextual info such as local git info, environment variables or directory name. A recipe can be a single command:
# .tmplr.yml
remove: LICENSE
Or multiple steps:
# .tmplr.yml
steps:
- read: project_name
from: git.remote_name
fallback:
from: filesystem.rootdir
- read: clone_url
from: git.remote_url
- update: README.md
π When you read a variable, it will be replaced in all the files copied / updated by the recipe. If README.md
looks something like this:
# {{ tmplr.project_name }}
This is my super awesome project. You can clone it using the following command:
```bash
git clone {{ tmplr.clone_url }}
```
And someone runs this recipe on their project, https://github.com/john/my-project
, then README.md
will become this:
# my-project
This is my super awesome project. You can clone it using the following command:
```bash
git clone https://github.com/john/my-project
```
Note
After you read a variable such as project_name
, in any file you update or copy, {{ tmplr.project_name }}
will be replaced with the value read. If a variable is not resolved, then tmplr
will leave it untouched.
Make sure any template variable you use starts with
tmplr.
prefix. tmplr will ignore any variable that doesn't.# β this is wrong, and `{{ project_name }}` will remain untouched project: name: {{ project_name }}# β this is correct. `{{ tmplr.project_name }}` will be replaced by the value read by the recipe. project: name: {{ tmplr.project_name }}
We have multiple commands and expressions used in the example recipe above:
steps
is a command that runs multiple other commands sequentially,read
is a command that reads a value into a variable,update
is a command that updates a file using read variables,from
is an expression, reading from contextual values such asgit.remote_url
orfilesystem.rootdir
.
Read more about the recipe syntax and available commands and expressions:
π More about recipe syntax
π€ Available commands
π Available expressions
π° Available pipes
π‘οΈ Available contextual values.
If you (like me) prefer learning by example, you can check this example template repository, or check these examples:
- Create GitHub template and run a recipe when someone uses your template
- Add new packages to monorepos using local templates
Recipes can access following contexts:
- Git Context
- Filesystem Context
- Environment Variables
- Date & Time
- Temporary Directories
- Recipe Arguments
Values related to git repository of the project. These only work inside a folder controlled by git.
git.remote_url
: The origin URL of current git repository (this can be cloned, for example)git.remote_name
: The name of the origin (e.g. repository name)git.remote_provider
: The address of the git host (e.g.https://github.com
)git.remote_owner
: The name of the user on the remote who owns the repositorygit.author_name
: The name of the person who made the first commit on the repogit.author_email
: Email address of the first committer.
Example:
# .tmplr.yml
steps:
- read: project_name
from: git.remote_name
fallback:
from: filesystem.rootdir
- read: author
from: git.author_name
fallback:
prompt: What is your name?
- read: repo_url
eval: 'https://{{ git.remote_provider }}/{{ git.remote_owner }}/{{ git.remote_name }}'
- update: package.json
- update: README.md
# ...
Warning
If the recipe is run outside of a repository (where there is no .git
), then git contextual values won't be available. Read git value using from
, and provide a fallback.
Warning
Even inside a git repository, if there are 0 commits, then git.author_name
and git.author_email
will be empty strings.
filesystem.root
: Absolute address of the root directory (which the recipe is being executed in)filesystem.rootdir
: The name of the root directoryfilesystem.scope
: Absolute address of the scope of this recipe.filesystem.scopedir
: The name of the scope directory.
The root directory, filesystem.root
, is where the recipe file is located. This is also the addrerss which all
relative addresses in the recipe are interpreted relative to. The scope of the recipe, filesystem.scope
, is where the recipe can access (read/write). The scope can be differnt from the root: when a recipe is called by another recipe (via run or use commands), the called recipe has the same
scope to the caller recipe, though their roots might differ.
Example:
# .tmplr.yml
steps:
# ...
# π will apply a reusable recipe to add proper license for the project.
# check the docs for the `use` command for more info.
- use: trcps/license
with:
owner: '{{ git.author_name }}'
project_name: '{{ filesystem.scopedir }}'
project_url: '{{ git.remote_url }}'
# ...
Use env.some_var
to access some environment variable. If it is not defined, an empty string will be returned.
datetime.now
: The current date and time in ISO format (e.g.2023-07-26T14:06:38.794Z
)datetime.date
: The current date (e.g.7/26/2023
).datetime.time
: The current time in the local timezone (e.g.5:15 PM
).datetime.year
: The current year (e.g.2023
).datetime.month
: The current month (e.g.6
).datettime.month_of_year
: The name of the current month (e.g.July
).datetime.day
: The current day (e.g.26
).datetime.day_of_week
: The name of the current day (e.g.Wednesday
).datetime.hour
: The current hour in the local timezone (e.g.17
).datetime.minute
: The current minute in the local timezone (e.g.15
).datetime.second
: The current time seconds (e.g.38
).datetime.millisecond
: The current time milliseconds (e.g.794
).
Tip
Use date & time pipes to further format date and time strings.
Use tmpdir.some_name
to automatically create temporary directories.
steps:
#
# some initial steps
#
- copy: some_file.go
to: '{{ tmpdir.go_file }}/some_file.go'
#
# some other steps
#
- copy: '{{ tmpdir.go_file }}/some_file.go'
to: some_other_file.go
Temporary directories will be deleted after the recipe has finished executing.
Recipes can also run other local recipes or use publicly published recipes. The caller recipe can pass arguments
to the called recipe, which will be available on args
context.
# called.yml
steps:
- read: remote_url
from: git.remote_url
- update:
path: '{{ args.readme }}'
# .tmplr.yml
steps:
- run: ./called.yml
with:
- readme:
path: ./README.md
Tip
Recipe arguments are evaluated lazily. If a prompt is passed as an argument, the user will be prompted the first time the argument is accessed, not when the recipe is called.
Recipes are composed of commands and expressions. Commands instruct actions (i.e. read a value, update a file, etc), and expressions calculate string values used by commands. A recipe descirbes a single command, which can itself be composed of multiple other steps:
# .tmplr.yml
remove: LICENSE
βοΈ Here the recipe is a single remove command.
# .tmplr.yml
steps:
- read: project_name
from: git.remote_name
fallback:
prompt: What is the name of the project?
default:
from: filesystem.rootdir
- update: README.md
βοΈ Here the recipe is a single steps command, which is composed of multiple steps (commands). Take a closer look at the initial read command:
- read: project_name
from: git.remote_name
fallback:
prompt: What is the name of the project?
default:
from: filesystem.rootdir
This command reads a variable, project_name
, from a contextual value. From this point on, you can use this variable in other expressions, pass it to other recipes you call, and when you copy or update a file, {{ tmplr.project_name }}
will be replaced with the variable's value. If the contextual value can't be resolved, it will fallback to a prompt, asking the user for the value, suggesting the name of the current directory as the default value.
Here you can see the corresponding syntax tree of this example recipe:
Steps Command
β
β£ββ Read Command
β β
β βββ From Expression
β β
β ββ(fallback)β Prompt Expression
β β
β ββ(default)β From Expression
β
ββ Update Command
β
ββ Value Expression
The string passed to the update command, README.md
, is also an expression, which means it could be replaced
by a prompt:
- update:
prompt: Which file do you want to update?
default: README.md
Or can reference variables / contextual values:
- update: '{{ readme_file }}.md'
- update: '{{ env.README_FILE }}.md'
Tip
For using variables in expressions (i.e. inside recipe files), you don't need the tmplr.
prefix. You can also directly access
contextual values such as git.remote_owner
, filesystem.rootdir
, or tmpdir.some_dir
directly. You can also use pipes to transform values.
Note that inside files that are copied or updated, you DO NEED the tmplr.
prefix, and you don't have access to other
contextual values. If you need to use these values within these files, read them into a variable first.
- read: reads a value into a variable, so that the variable can be used to update subsequent files.
- update: updates contents of some files, using variables read.
- copy: copies some files, also updating them using variables read.
- write: writes given content to a file.
- remove: removes some files.
- steps: runs a bunch of commands in a step by step manner.
- if: runs a command conditionally.
- skip: skips current steps or recipe.
- degit: copies content of given repository to given folder.
- run: runs another local recipe file, with given arguments.
- use: runs a remote recipe file, with given arugments.
Command
read: <variable name> <expression>
Reads some value into a variable. The variable can then be used in subsequent expressions or passed as an argument to other called recipes. It will also be replaced in all files that are updated or copied.
steps:
- read: project_name
from: filesystem.rootdir
βοΈ After executing this command, if you update or copy any file that contains {{ tmplr.project_name }}
, the value of the variable will be replaced.
Command
update: <expression> include hidden?: <boolean>
Updates a file, using variables that are already read
.
steps:
- read: name
prompt: What is your name?
- update: README.md
steps:
- read: docs_folder
prompt: Where do you keep the docs?
choices:
- docs
- documents
- other:
prompt: Specify the folder name ...
- update:
path: '{{ docs_folder }}/Home.md'
π Pass an extended glob pattern to update multiple files at once:
update: 'src/**/*.java'
When using a glob pattern, hidden files (starting with a dot, e.g. .gitignore
) and files in hidden folders (e.g. .github/workflows/publish.yml
) are ignored. Update hidden files by explicitly mentioning them:
- update: '**/.*.java'
- update: '**/.**/**/*.java'
Or by using the include hidden
option:
update: '**/*'
include hidden: true
Command
copy: <expression> to: <expression> include hidden?: <boolean>
Copies a file, creating necessary folders, replacing existing files. Will also update the copied file, replacing all read variables with their values.
steps:
- read: email
from: git.author_email
- copy: .template/CODE_OF_CONDUCT
to: CODE_OF_CONDUCT
steps:
- read: email
from: git.author_email
- degit: some/license_template
to:
path: '{{ tmpdir.license }}'
- copy:
path: '{{ tmpdir.license }}/LICENSE'
to: LICENSE
π Pass an extended glob pattern to copy multiple files at once. When
copying multiple files, the to
expression is treated as a folder address:
copy: ./template/code/**/*.java
to: src/main/java
βοΈ The structure of the copied files will be preserved in the destination folder. In the above example,
./template/code/com/example/Hello.java
will be copied to src/main/java/com/example/Hello.java
.
When using a glob pattern, hidden files (starting with a dot, e.g. .gitignore
) and files in hidden folders (e.g. .github/workflows/publish.yml
) are ignored by default. Copy hidden files by explicitly mentioning them:
- copy: '**/.*.java'
to: src/main/java
- copy: '**/.**/**/*.java'
to: src/main/java
Or by using the include hidden
option:
copy: '**/*.java'
to: src/main/java
include hidden: true
Command
write: <expression> to: <expression>
Writes given content to a file, creating necessary folders, replacing existing files. Will replace all read variables with their values inside the written content (not the whole file).
steps:
- read: badge_content
from file: .template/badge.md
- read: readme_content
from file: README.md
- write: '{{ badge_content }}\n{{ readme_content }}'
to: README.md
Command
remove: <expression> include hidden?: <boolean>
Removes a file or a folder.
steps:
# do some other stuff
- remove: .tmplr.yml
π Pass an extended glob pattern to remove multiple files at once:
remove: ./**/*.tmplr.*
When using a glob pattern, hidden files (starting with a dot, e.g. .gitignore
) and files in hidden folders (e.g. .github/workflows/publish.yml
) are ignored by default. Remove hidden files by explicitly mentioning them:
- remove: '**/.*'
- remove: '**/.**/**/*'
Or by using the include hidden
option:
remove: '**/*'
include hidden: true
Note that when passing a glob pattern, folders are not removed. If you want to remove a folder, you need to pass the folder path explicitly:
remove: .github
Command
steps: - <command> - <command> - ...
Runs given commands step by step.
steps:
- read: name
from: git.author_name
fallback:
from: env.USER
- update: package.json
- copy: .template/README.md
to: README.md
- remove: .template
Command
if: <variable / contextual value> <command> else?: <command>if: <expression> <command> else?: <command>if not: <variable / contextual value> <command> else?: <command>if not: <expression> <command> else?: <command>
Runs given command if given variable, contextual value, or expression resolves to a non-empty string. Runs the else command if the condition fails.
steps:
- if: git.remote_url
copy: README.git-template.md
to: README.md
else:
copy: README.non-git-template.md
to: README.md
Can also be used as a ternary operator:
prompt: Wassup?
default:
if: some_var
eval: 'Hello {{ some_var }}!'
else:
eval: 'Hello world!'
Command
skip: stepsskip: recipe
Skips the rest of current steps or recipe. Useful for conditional execution:
steps:
# ...
- prompt: Are you sure?
choices:
- Yes
- No:
skip: recipe
# ...
Command
degit: <expression> to?: <expression> subgroup?: <boolean>
Copies contents of given repository into specified folder (using degit). If destination is not specified, will copy into the same folder as the running recipe. Accepts the same sources as tmplr
command.
steps:
- degit: user/repo
to:
eval: '{{ tmpdir.repo }}'
Tip
For cloning gitlab subgroups, use the subgroup
option.
Command
run: <expression> with?: <argname>: <expression> <argname>: <expression> ... read?: <varname>: <outname> <varname>: <outname> ...
Parses and executes given local recipe. You can pass arguments to the recipe file (which can be accessed via the args
context lazily). The recipe WILL NOT have access to variables you have read by default. You can read the variables read by the recipe into variables in your own recipe.
steps:
- read: name
from: git.author_name
- run: .templates/util/some-recipe.yml
with:
name: name # will pass `name` variable
remote_url:
from: git.remote_url # this will be executed lazily
fallback:
prompt: What is the remote URL?
read:
lockfile: lockfile # will read `lockfile` variable of the inner recipe into `lockfile` variable of outer recipe
some_success: success # will read `success` variable of the inner recipe into `some_success` variable of outer recipe
Important
Relative paths are resolved relative to the recipe. In the example above, the caller recipe referencing README.md
will access README.md
at the root of the project, while the called recipe accessing README.md
would access .templates/util/README.md
. It is recommended to use the path expression to turn all path strings into absolute paths.
π€‘ USELESS FACTOID
When running
tmplr owner/repo
, tmplr basically runs the following recipe:steps: - degit: owner/repo to: . - run: .tmplr.yml
Command
use: <expression> with?: <argname>: <expression> <argname>: <expression> ... read?: <varname>: <outname> <varname>: <outname> ...
Runs given reusable recipe. For example, the following will help users add a licence to their project:
steps:
# ...
- use: trcps/license
with:
owner: '{{ git.author_name }}'
project_name: '{{ filesystem.scopedir }}'
project_url: '{{ git.remote_url }}'
# ...
use
downloads, parses and executes given recipe from a public repository. It fetches the specified repository (using degit) into a temporary directory at the root of the project, locates .tmplr.yml
in that directory and runs it, and removes the directory. The specified repository MUST have a .tmplr.yml
file at its root.
steps:
- read: name
from: git.author_name
- use: some-user/some-repo
with:
name: name # will pass `name` variable
remote_url:
from: git.remote_url # this will be executed lazily
fallback:
prompt: What is the remote URL?
read:
lockfile: lockfile # will read `lockfile` variable of the inner recipe into `lockfile` variable of outer recipe
some_success: success # will read `success` variable of the inner recipe into `some_success` variable of outer recipe
π‘ Read this section to learn more about creating reusable recipes.
- from: reads from a contextual value.
- prompt: asks the value from user.
- choices: asks the value from user, but gives them some predetermined choices.
- eval: evaluates an expression.
- path: evaluates to an absolute path value.
- exists: checks if a file exists or not.
- from file: reads content of a file.
Expression
from: <contextual-variable> fallback?: <expression>
Resolves given contextual value. If it can't be resolved, will evaluate the fallback expression, or an empty string if no fallback is specified.
steps:
- read: username
from: git.remote_owner
fallback:
from: env.USER
Expression
prompt: <message> default?: <expression>
Asks the user for a value. If a default value is provided, then that will be suggested to the user as well.
steps:
- read: username
prompt: What is your username?
default:
from: git.author_name
fallback:
from: env.USER
Expression
prompt: <message> choices: - <label>: <expression> - <label>: <expression> ...
Asks the user to choose from a list of values. Evaluates the corresponding expression of each choice after the user has selected it (so you can chain prompts and other expressions safely).
steps:
- read: username
prompt: What is your username?
choices:
- Read it from git:
from: git.author_name
- Read it from env:
from: env.USER
- John Doe # π here the value is the same as the label.
- None:
prompt: Ok but what is your username though?
Expression
eval: <expression> steps?: - <command> - <command> ...
Evaluates given expression, similar to evaluation of template variables in updated or copied
files, except you don't need the tmplr.
prefix, and can access contextual values too.
steps:
read: git_url
from: git.remote_url
fallback:
eval: 'https://github.com/{{ env.USER | snake_case }}/{{ filesystem.rootdir }}.git'
You can optionally pass a list of commands as the steps property. These are usually (but not necessarily) some reads to fetch further values required for the evaluation. Note that these commands only get executed if the Eval Expression itself is evaluated.
steps:
- read: git_url
from: git.remote_url
fallback:
steps:
- read: git_provider
prompt: Where is the project hosted?
choices:
- GitHub: 'https://github.com'
- BitBucket: 'https://bitbucket.org'
- Source Hut: 'https://git.sr.ht'
- Other:
prompt: Please specify ...
- read: git_owner
from: env.USER
fallback:
prompt: What is your username?
eval: '{{ git_provider }}/{{ git_owner }}/{{ filesystem.rootdir }}.git'
Expression
path: <expression>
Similar to eval but for strings representing file paths. If the expression evaluates to a relative path, will turn it into an absolute path (relative paths are relative to the recipe). Use it to pass path arguments to and reading path values from recipes you use or run.
steps:
# ...
- degit: some/repo
to:
eval: '{{ tmpdir.some_repo }}'
- use: some/recipe
with:
readme:
path: '{{ tmpdir.some_repo }}/README.md'
Expression
exists: <expression> include hidden?: <boolean>
Checks if a file exists or not. Can be passed a glob pattern, in which case checks if any file matching given pattern exists or not. If it does, returns the path of the first matching file.
steps:
- if:
exists: '**/*'
prompt: 'Directory is not empty. Overwrite?'
choices:
- Yes
- No:
skip: recipe
# ...
Similar to copy, update and remove, the command will by default ommit hidden files unless explicitly mentioned in the glob pattern. Override this using include hidden
property:
exists: '**/*'
include hidden: true
Important
exists
only checks for existence of files, and ignores directories. So this is wrong:
# β WRONG
exists: 'my-dir/'
Use **/*
glob pattern instead:
# β
CORRECT
exists: 'my-dir/**/*'
Expression
from file: <expression>
Reads the content of a file.
steps:
- read: readme
from file: README.md
Use pipes to modify variables, either in copied / updated files, or in the recipe itself:
# .tmplr.yml
steps:
- read: name
prompt: whats the name?
- copy: .templates/template.md
to: '{{ name | path/case }}.md'
- remove: .templates
<!-- .templates/template.md -->
# {{ tmplr.name | Capital Case }}
This is a super awesome project that can be installed by running:
```bash
npm i {{ tmplr.name | kebab-case }}
```
βοΈ Running this recipe with the name cool project
will result in cool/project.md
with the following contents:
# Cool Project
This is a super awesome project that can be installed by running:
```bash
npm i cool-project
```
Use the following pipes to change the casing of a string (they are case-sensitive):
- camelCase - Capital Case - CONSTANT_CASE
- dot.case - Header-Case - kebab-case
- PascalCase - path/case - param-case
- Sentence case - UPPERCASE - lowercase
Use skip
and trim
pipes to remove the given number of characters from the beginning and
the end of the string, respectively:
steps:
- read: component_name
prompt: What is the name of the component?
default:
#
# if the directory name is 'react-my-component', then
# this will evaluate to 'MyComponent'.
#
eval: '{{ filesystem.rootdir | skip: 6 | PascalCase }}'
π Pass string values to trim
and skip
to remove the given string
from the start / end of the variable. Only works if the variable starts / ends with exactly the given string:
steps:
- read: component_name
prompt: What is the name of the component?
default:
#
# if the directory name is 'react-my-component', then
# this will evaluate to 'MyComponent'. However, if the
# directory name does not start with 'react-', then it will
# not modify it.
#
eval: '{{ filesystem.rootdir | skip: react- | PascalCase }}'
Use date format
to format a value representing some date:
steps:
- read: date
eval: '{{ datetime.now | date format: YYYY-MM-DD }}'
- update: LICENSE
Use time format
to format a time string:
steps:
- read: time
eval: '{{ datetime.now | time format: HH:mm:ss }}'
Use datetime format
to format both:
read: now
eval: '{{ datetime.now | datetime format: YYYY-MM-DD HH:mm:ss }}'
π‘ Read this to learn more about possible formats.
π To format date / time using locale specific formats, pass locale <locale>
to any of the pipes:
read: now
eval: '{{ datetime.now | datetime format: locale en-US }}'
read: zeit
eval: '{{ datetime.now | time format: locale de }}'
π‘ Language and locale codes are based on this and this standards. You can use tools like this to figure out which tags you should use.
Use matches
pipe to check if a variable matches given string/pattern. This pipe returns the given string if it matches, and returns an empty string otherwise. Use this for conditional commands:
steps:
# ...
- if:
eval: '{{ some_var | matches: some value }}'
update: some_file.txt
else:
update: some_other_file.txt
# ...
if:
eval: '{{ database | matches: /Mongo/ }}'
copy: mongodb.config.js
to: ./src/config/db.config.js
else:
copy: postgres.config.js
to: ./src/config/db.config.js
A reusable recipe is similar to a template, except that it should change only a specific part of a project. They might be applied directly, or used as part of another recipe.
For example, this reusable recipe adds a GitHub action to automatically publish to NPM on a version bump. It can be directly applied to a project like this:
tmplr use trcps/npm-autopublish
Or it can be used as part of another recipe:
steps:
# ...
- use: trcps/npm-autopublish
# ...
Making a reusable recipe is similar to making a template repository, with following differences:
- Your repository will be cloned to a temporary directory, not the root of the project.
- This temporary directory will be removed when your recipe is finished running.
π Copy any files you want to add to the project explicitly:
# inside some reusable recipe ...
steps:
- copy: workflow.yml
to: ../.github/workflows/publish.yml
π Read or write files from the host project using ../file
. OR, use filesystem scope and path:
steps:
- copy: workflow.yml
to:
path: '{{ filesystem.scope }}/.github/workflows/publish.yml'
π Use tmplr preview:use
to see what would happen if your repo was used as a reusable recipe:
tmplr preview:use
This command applies your recipe to an empty .tmplr-preview
directory, where you can inspect the results.