npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

md-tmpl

v0.7.1

Published

Strongly-typed template engine for LLM prompts, designed for markdown

Readme

md-tmpl

Strongly-typed prompt templates for LLMs.

The Cool Part

Define a prompt as a .tmpl.md file — readable markdown with typed frontmatter:

---
consts:
  - MAX_RETRIES = int := 3

types:
  - Priority = enum(High, Medium, Low)

params:
  - role = str
  - tasks = list(title = str, priority = Priority)
  - outcome = enum(Confirmed(evidence = str), Rejected)
---

You are {{ role }}. You have {{ len(tasks) }} tasks:

> {% for task in tasks %}

- **{{ task.title }}** ({{ task.priority | upper }})

> {% /for %}

> {% match outcome %}
> {% case Confirmed %}

✅ Confirmed — {{ outcome.evidence }}

> {% case Rejected %}

❌ Rejected. Retry up to {{ MAX_RETRIES }} times.

> {% /match %}

Generate TypeScript types from that file, then render with full type safety:

import type { Params } from "./generated/agent_task.js";
import { TypedTemplate, defineVariants, match } from "md-tmpl";

const tmpl = TypedTemplate.fromFile<Params>("prompts/agent_task.tmpl.md");

const Outcome = defineVariants({
  Confirmed: ["evidence"],
  Rejected: null,
});

const result = tmpl.render({
  role: "a code review agent",
  tasks: [
    { title: "Check error handling", priority: "High" },
    { title: "Verify test coverage", priority: "Medium" },
  ],
  outcome: Outcome.Confirmed({ evidence: "All edge cases covered" }),
});

// Pattern-match the outcome later
const summary = match(outcome, {
  Confirmed: (f) => `✅ ${f.evidence}`,
  Rejected: () => "❌ Needs revision",
});

The generated types look like this — interfaces, enums, constants, and defaults:

export interface TasksItem {
  readonly title: string;
  readonly priority: Priority;
}

export type Priority = "High" | "Medium" | "Low";

export interface Outcome_Confirmed {
  readonly __kind__: "Confirmed";
  readonly evidence: string;
}

export type Outcome = Outcome_Confirmed | "Rejected";

export interface Params {
  readonly role: string;
  readonly tasks: readonly TasksItem[];
  readonly outcome: Outcome;
}

export const CONSTANTS = {
  MAX_RETRIES: 3,
} as const;

Why?

  • Markdown-native — prompts live in .tmpl.md files, readable in any editor or on GitHub. No embedded strings, no escaping.
  • Strict typing — every parameter declares a type; generateTypes() emits TypeScript interfaces from frontmatter. Catch errors before deployment, not at 3 AM in production.
  • Agent-safe — when an LLM edits prompts, the engine catches drift immediately. Renamed a field? Changed a type? Broke the contract? You'll know before it ships.

Installation

Available on npm: https://www.npmjs.com/package/md-tmpl

npm install md-tmpl

Type Generation

generateTypes() turns frontmatter into full TypeScript — interfaces, enums, constants, and defaults. Run it as a build step:

import { generateTypesFromFile } from "md-tmpl";
import * as fs from "node:fs";

fs.writeFileSync(
  "src/generated/agent_task.ts",
  generateTypesFromFile("prompts/agent_task.tmpl.md"),
);

Or generate from a raw source string:

import { generateTypes } from "md-tmpl";

const code = generateTypes(`---
consts:
  - MAX_RETRIES = int := 3

params:
  - message = str
  - level = int := 1
  - tasks = list(title = str, priority = str)
  - outcome = enum(Confirmed(evidence = str), Rejected)
---
{{ message }}`);

Output includes Params, item interfaces, enum unions, CONSTANTS, and DEFAULTS — ready to import.

Type Mapping

| Frontmatter Type | TypeScript Type | | :-------------------------- | :---------------------------------------------- | | str | string | | int | number | | float | number | | bool | boolean | | list(field = type, ...) | readonly GeneratedItem[] | | list(type) | readonly T[] (e.g. readonly string[]) | | struct(field = type, ...) | GeneratedInterface | | enum(Variant, ...) | Union type (Variant1 \| Variant2_Struct) | | option(type) | T \| null (T \| undefined is also accepted) | | tmpl(...) | ITemplate (or callable template reference) |

Enum Dispatch

defineVariants creates type-safe enum constructors. match gives you exhaustive pattern matching. isVariant is a type guard.

import { defineVariants, match, isVariant } from "md-tmpl";

const Status = defineVariants({
  Done: ["summary"],
  InProgress: null,
  Blocked: ["reason"],
});

// Construct
const status = Status.Done({ summary: "All tests pass" });

// Pattern match — exhaustive over all variants
const msg = match(status, {
  Done: (f) => `✅ ${f.summary}`,
  InProgress: () => "🔄 Working...",
  Blocked: (f) => `❌ ${f.reason}`,
});

// Wildcard fallback
const simple = match(status, {
  Done: (f) => f.summary as string,
  _: () => "pending",
});

// Type guard
if (isVariant(status, "Done")) {
  console.log("Completed!");
}

Features

Typed Lists

const tmpl = Template.fromSource(`---
params:
  - tasks = list(title = str, priority = str)
---
> {% for task in tasks %}

- {{ task.title }}: {{ task.priority }}

> {% /for %}`);

tmpl.render({
  tasks: [
    { title: "Write documentation", priority: "High" },
    { title: "Add unit tests", priority: "Medium" },
  ],
});

Default Values

const tmpl = Template.fromSource(`---
params:
  - greeting = str := "Hello"
  - name = str
---
{{ greeting }}, {{ name }}!`);

tmpl.render({ name: "Alice" }); // → "Hello, Alice!"
tmpl.defaults(); // → { greeting: "Hello" }

Environment Variables

Inject values at compile time from the build environment using env: declarations. Env vars are resolved once when the template is compiled and behave like constants at render time.

const tmpl = Template.fromSourceWithEnv(
  `---
params:
  - name = str
env:
  - MODEL = str
  - MAX_TOKENS = int := 4096
---
Hello {{ name }}! Using {{ MODEL }} (max {{ MAX_TOKENS }} tokens).`,
  { env: { MODEL: "gemini-2.0-flash" } },
);

tmpl.render({ name: "Alice" });
// → "Hello Alice! Using gemini-2.0-flash (max 4096 tokens)."

Or load from a file with env:

const tmpl = Template.fromFileWithEnv("prompts/agent.tmpl.md", {
  env: { MODEL: "gemini-2.0-flash", MAX_TOKENS: "8192" },
});

If/Elif/Else

> {% if level == 1 %}

Beginner: {{ name }}

> {% elif level == 2 %}

Intermediate: {{ name }}

> {% else %}

Expert: {{ name }}

> {% /if %}

Filters

{{ name | upper }}        → ALICE
{{ name | lower }}        → alice
{{ name | trim }}         → (strips whitespace)
{{ score | fixed(2) }}    → 3.14
{{ items | join(", ") }}  → a, b, c
{{ items | limit(2) }}    → first 2 elements
{{ count | add(1) }}      → 43
{{ count | sub(1) }}      → 41
{{ name | trim | upper }} → chains work

Built-in Functions

{{ len(items) }}          → 3 (list length)
{{ len(name) }}           → 5 (string length)
{{ idx(item) }}           → 0, 1, 2, … (loop index)
{{ kind(status) }}        → "Done" (variant name)
{{ kinds(Status) }}       → ["Done", "InProgress", "Blocked"] (all variant names)
{{ has(field) }}          → true if option(T) is present

API Reference

Template

// Constructors
Template.fromSource(source: string): Template
Template.fromSourceWithBaseDir(source, dir): Template
Template.fromSourceWithEnv(source, env: Record<string, unknown>): Template
Template.fromSourceWithOptions(source, options: CompileOptions): Template
Template.fromFile(path: string): Template
Template.fromFileWithEnv(path, options?: CompileOptions): Template

// Rendering
tmpl.render(params, options?)        // type-validated
tmpl.renderEmpty()                   // render with defaults only (no params)
tmpl.renderUnchecked(params)         // skip ALL validation (fastest)
tmpl.renderDict(params, options?)    // from Map or Record
// options: { allowExtra?: boolean, allowUnused?: boolean }

// Metadata
tmpl.declarations()                  // → [["name", "str"], ["count", "int"]]
tmpl.sourceHash()                    // content hash
tmpl.body()                          // template body after frontmatter
tmpl.defaults()                      // → { count: 5 }
tmpl.consts()                        // → { MAX_RETRIES: 3 }
tmpl.frontmatter                     // parsed frontmatter
tmpl.setMaxIncludeDepth(depth)
tmpl.validateDeclarationsAgainst(expected)

TypedTemplate<P>

TypedTemplate.fromSource<P>(source): TypedTemplate<P>
TypedTemplate.fromFile<P>(path): TypedTemplate<P>

tmpl.render(params: P)              // TS type-checked + runtime validated
tmpl.renderUnchecked(params: P)     // skip runtime validation, trust TS types
tmpl.renderTrusted(params: P)       // validate first call, skip rest

TemplateCache

const cache = new TemplateCache();
const tmpl = cache.load("prompts/greeting.tmpl.md");
cache.templateCount();
cache.clear();

ITemplate Interface

Both Template and the WASM Template implement ITemplate. Write backend-agnostic code:

import type { ITemplate } from "md-tmpl";

function renderGreeting(tmpl: ITemplate, name: string): string {
  return tmpl.render({ name });
}

Performance

Internal Benchmarks

Node.js 22, single-template timings (lower is better):

| Scenario | render | renderUnchecked | | ------------------------ | --------: | --------------: | | simple (1 str) | 623 ns | 510 ns | | multi-param (4 types) | 1,901 ns | 1,090 ns | | list (2 items) | 3,810 ns | 2,136 ns | | list (20 items) | 27,245 ns | N/A | | enum unit variant | 18,371 ns | N/A | | enum struct variant | 1,714 ns | N/A | | filters (idx+add, upper) | 6,552 ns | N/A | | if/elif/else | 1,919 ns | 1,557 ns |

renderUnchecked() skips runtime type validation — use it when TypeScript's static checks are sufficient.

Comparison Benchmarks

Node.js 22, 50,000 iterations, best of 5 runs (source).

Render only (pre-parsed template + data → output):

| Scenario | render() | renderUnchecked() | Handlebars | Mustache | | ------------------ | -------: | ----------------: | ---------: | --------------: | | simple | 1,040 ns | 814 ns | 1,258 ns | 745 ns 🏆 | | loop (5 items) | 5,233 ns | 2,850 ns | 1,897 ns | 1,863 ns 🏆 | | conditional | 2,442 ns | 2,110 ns | 1,549 ns | 450 ns 🏆 |

Round-trip (parse + render):

| Scenario | md-tmpl | Handlebars | Mustache | | ------------------ | --------: | ---------: | --------------: | | simple | 9,276 ns | 80,225 ns | 934 ns 🏆 | | loop (5 items) | 19,706 ns | 113,190 ns | 1,861 ns 🏆 |

A WASM build (~200 KB .wasm) is also available for exact feature parity. Pure TypeScript is 2–6× faster for small templates due to JS↔WASM serialization overhead; WASM closes the gap on complex templates.

just bench-ts           # internal benchmarks
just bench-ts-compare   # vs Handlebars & Mustache

Testing

just test-ts    # 935 tests
just lint-ts    # strict type-check with tsc
just fmt-ts     # format with prettier

Full Reference

See SPEC.md for the complete syntax reference.

License

Apache-2.0 OR MIT