Skip to content

loreanvictor/tmplr

Repository files navigation

version tests

       β”“
 ╋┏┳┓┏┓┃┏┓  repo
 ┗┛┗┗┣┛┗┛   templating πŸš€
     β”›

Create projects from interactive templates, or enrich existing projects using interactive recipes.

Demo

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 git@bitbucket.org: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.

Contents


Installation

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

Usage

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 git@gitlab.com:owner/repo
tmplr https://gitlab.com/owner/repo
tmplr gitlab:owner/group/repo --subgroup

# πŸͺ£ download from BitBucket
tmplr bitbucket:owner/repo
tmplr git@bitbucket.org:owner/repo
tmplr https://bitbucket.org/owner/repo

# πŸ›– download from Sourcehut
tmplr git.sr.ht/owner/repo
tpmlr git@git.sr.ht: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

Running Recipes

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

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.


Working Directory

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.


Execution Safety

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.



Making a Template

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.


Template Recipes

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:

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:


Contextual Values

Recipes can access following contexts:


Git Context

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 repository
  • git.author_name: The name of the person who made the first commit on the repo
  • git.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 Context

  • filesystem.root: Absolute address of the root directory (which the recipe is being executed in)
  • filesystem.rootdir: The name of the root directory
  • filesystem.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 }}'

  # ...

Environment Variables

Use env.some_var to access some environment variable. If it is not defined, an empty string will be returned.


Date & Time

  • 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.


Temporary Directories

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.


Recipe Arguments

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.


Recipe Syntax

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.


Commands

  • 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.

Read

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.


Update

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

Copy

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

Write

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

Remove

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

Steps

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

If

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!'

Skip

Command

skip: steps
skip: recipe

Skips the rest of current steps or recipe. Useful for conditional execution:

steps:
  # ...

  - prompt: Are you sure?
    choices:
      - Yes
      - No:
          skip: recipe

  # ...

Degit

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.


Run

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

Use

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.


Expressions

  • 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.

From

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

Prompt

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

Choices

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?

Eval

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'  

Path

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'

Exists

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/**/*'

From File

Expression

from file: <expression>

Reads the content of a file.

steps:
  - read: readme
    from file: README.md

Pipes


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
```

Letter Case Pipes

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

String Pipes

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 }}'

Date & Time Pipes

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.


Regexp Matching

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

Making a Reusable Recipe

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.