Creating a Template Auto Generator in Typescript

4 min readOct 1, 2024
We’re building this

Coding is a tedious tasks especially if you’re doing the same work over and over again. So I recently tried creating a code auto generator, similar to PHP’s artisan which can make bunch of working codes in a single terminal command. In this article, we’re only going to create a simple generator to generate HTML codes which will include looping, and functions.

Technology Stack

  • typescript
  • html
  • bun (you can use npm, yarn, or other similar tools but you have to setup typescript)

Problem Statement

Let’s imagine that we an HTML code that looks like this:

// customers.html

<h1>Customers</h1>
<ul>
<li>Customer 1</li>
<li>Customer 2</li>
</ul>
<p>Currently we have 2 customers</p>

And another one like this:

// cars.html

<h1>Cars</h1>
<ul>
<li>Car 1</li>
<li>Car 2</li>
<li>Car 3</li>
</ul>
<p>Currently we have 3 customers</p>

If there’s only two files, that’s fine, but what if in the lifespan of our project, we wanted to make 100 files like this one, each with their own unique naming?

Here’s how code generation will become handy. Since we can identify a lot of the common stuffs that consists in a file, we can create templates that just needs little prompt to make it work.

Code Structure

generator/
|- index.template.html
|- generator.ts
// ... other bun related files we won't be changing this)

Setup

  1. In a new directory, run bun init
  2. In package.json , add a new script: "generate": "bun ./generator.ts"
  3. Remove index.ts
  4. Create generator.ts and index.template.html

Step 1 : Tokenization

Using the example above, we can conclude that there are 4 things that’s recurrence:

  • The feature name ( customer )
  • The feature name, capitalized ( Customer )
  • The feature name, plural ( customers )
  • The feature name, plural and capitalized ( Customers )

Let’s map those into tokens that can be reusable throughout the template:

Now let’s put these token into work to the template:

// index.template.html
<h1>$CAPITALIZED_PLURAL_NAME$</h1>
<ul>
<li>$CAPITALIZED_NAME$ 1</li>
<li>$CAPITALIZED_NAME$ 2</li>
</ul>
<p>Currently we have 2 $PLURAL_NAME$</p>

You might realize that the $FEATURE_NAME$ variable itself isn’t used. But it’ll be clear later that the $FEATURE_NAME$ token is actually the source for all these other tokens. For now, keep that token in.

(Scripting)

The scripting side of thing have to do several things. Here is how the flow looks like:

  1. Get input from terminal to determine token
    (bun generate customer will generate tokens based on customer)
  2. Create token : value mapping
  3. Read index.template.html , store it to a string buffer
  4. Replace the texts in the template with the token
  5. Save the result into a file

In the typescript code, it’ll look like this:

// generator.ts
import path from "path";

const templatePath = path.join(process.cwd(), "index.template.html");

const main = async () => {
const featureName = getFeatureNameFromArgs();
const tokens = generateTokens(featureName);
const template: string = await readTemplateFile(templatePath);
const result = replaceTokens(template, tokens);
await writeToOutputFile(featureName, result);
};

main();

function getFeatureNameFromArgs(): string {
const args = process.argv;
if (args.length < 3) {
console.error("Please provide a feature name as an argument.");
process.exit(1);
}
return args[2];
}

function generateTokens(featureName: string): Record<string, string> {
const capitalized =
featureName.charAt(0).toUpperCase() + featureName.slice(1);
const plural = pluralize(featureName);
const capitalizedPlural = pluralize(capitalized);

const tokens = {
FEATURE_NAME: featureName,
CAPITALIZED_NAME: capitalized,
PLURAL_NAME: plural,
CAPITALIZED_PLURAL_NAME: capitalizedPlural,
};
console.log(tokens);
return tokens;
}

function pluralize(str: string): string {
// This is a very simple pluralization function and doesn't cover all cases
if (str.endsWith("y")) {
return str.slice(0, -1) + "ies";
} else if (
str.endsWith("s") ||
str.endsWith("x") ||
str.endsWith("z") ||
str.endsWith("ch") ||
str.endsWith("sh")
) {
return str + "es";
} else {
return str + "s";
}
}

async function readTemplateFile(path: string) {
let template: string;
try {
template = await Bun.file(path).text();
} catch (error: any) {
console.error(`Error reading template file: ${error.message}`);
process.exit(1);
}
return template;
}

function replaceTokens(
template: string,
tokens: Record<string, string>
): string {
let result = template;
for (const [token, value] of Object.entries(tokens)) {
result = result.replace(new RegExp(`\\$${token}\\$`, "g"), value);
}
return result;
}

async function writeToOutputFile(featureName: string, result: string) {
const outputPath = path.join(process.cwd(), `${featureName}.html`);
try {
await Bun.write(outputPath, result);
console.log(`Generated file saved as: ${outputPath}`);
} catch (error: any) {
console.error(`Error writing output file: ${error.message}`);
process.exit(1);
}
}

Now you can call the script using:

bun generate customer

It will generate customer.html . Here’s the output in the terminal:

One thing to improve is that the token replacement process will take longer and longer since replaceTokens loop the whole content (n) times to find regexp. This can be smaller if we implement more efficient string search and replacement, but I’ll just leave it here for now.

Hope this helps in understanding how templating works, thank you!

A lot of credit goes to claude for helping me make the script.

P.S. If you’re interested I can make part 2 about templating function & looping. Let me know in the comments.

--

--

Mario Gunawan
Mario Gunawan

Written by Mario Gunawan

I'm a mobile / web developer. I'm mostly writing articles about software development.

No responses yet