Reverse values into variables in tmplr

tmplr is a versatile templating tool that lets you quickly create a repository from a template customized for your needs. By running a recipe it can substitute placeholder variables with your own values making the repository truly your own. For example it can update package.json with dynamic values from git context. It can do much more, put for the purpose of this post we will concentrate on literal values in code files.

Let’s say you have a JavaScript code that performs some AWS SDK/CDK actions, and it requires AWS account IDs – for dev and prod deployment. In a regular repo you would define config.js something like this:

const awsAccounts = {
   dev: '1234567890',
   prod: '0987654321'
}

But if we’re building a template repo – we don’t want to hardcode those values. Instead we could use tmplr placeholder variables:

const awsAccounts = {
   dev: '{{ tmplr.dev_account_id }}',
   prod: '{{ tmplr.prod_account_id }}'
}

Save this file as config.tmplr.js – it becomes our template. A user of your template then can create a tmplr recipe:

steps:
  - read: dev_account_id
    eval: '1234567890'
  - read: prod_account_id
    eval: '0987654321'
  - copy: config.tmplr.js
    to: config.js

then save this it into .tmplr.yml file into repo root, and run tmplr cli command – it will create config.js file from config.tmplr.js substituting variables for values from the recipe.

It works great, but there is a problem.

If you’re actively developing the template – live code file (in our case config.js) that your template is based on will be changing – code will be added/removed/refactored. This means that the config.tmplr.js template will become outdated, will be out of sync with config.js. We need a way to update our template with the latest code. Luckily there is a way to automate this, so you don’t have to manually copy pieces of code from the live file to the template.

Basically we need an opposite of what tmplr does – take the file that has literal values, replace them with placeholder variables, and save it as a template. As far as I know as of the time of this post tmplr doesn’t have this capability, so I put together a small JavaScript helper (you will need to install one dependency 'js-yaml' to handle YAML files):

const yaml = require('js-yaml');
const fs = require('fs');
 
const tmplr = yaml.load(
      fs.readFileSync(`.tmplr.yml`, 'utf-8')).steps;
const files = tmplr.filter((step) => 'copy' in step);
const variables = tmplr.filter((step) => 'read' in step);
 
for (let file of files) {
  let fileContent = fs.readFileSync(file.to, 'utf-8');
 
  for (let variable of variables) {
    fileContent =
      fileContent.replaceAll(variable.eval, 
         `{{ tmplr.${variable.read} }}`);
  }
 
  fs.writeFileSync(
       file.copy, fileContent, { encoding: 'utf8', flag: 'w' });
  console.log(`✔ Templated: "${file.to}" -> "${file.copy}"`);
}

This code uses the same .tmplr.yml file we created earlier for tmplr (Lines 04-07 read and parse the YAML file, and extract variable/value, copy_from/copy_to pairs from it). It then loops thru the files, loading live code file in every iteration (Lines 09-10). For each file it loops thru variables/values, and if it finds literal value – replaces it with placeholder variable (Lines 12-16). And finally it writes resulting template into template file (Lines 18-19)

You can run this script manually, or – if you’re using VS Code using Run On Save extension, or even in a GitHub workflow on push of a changed file. In any case your template files will be always in sync with your code files.

Leave a Reply

Your email address will not be published. Required fields are marked *