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

typescript-mdma

v0.7.0

Published

Typed Markdown templating: render one or more Markdown strings from an .mdma template and an inputs object.

Readme

typescript-mdma

Render one or more Markdown strings from an .mdma template and a typed inputs object.

This is the TypeScript reference implementation of MDMA. See the language specification and docs for the full grammar, filter reference, and worked examples.

Install

npm install typescript-mdma

Usage

import { render } from "typescript-mdma";
import { readFileSync } from "node:fs";

const source = readFileSync("release-notes.mdma", "utf-8");
const result = render(source, {
  project: "Acme SDK",
  version: "3.0.0",
  date: "2026-07-01",
  added: ["WebSocket support"],
  breaking: true,
  releases: [{ version: "2.1.0", date: "2026-06-01", added: ["Dark mode"] }],
});

result.slug;             // "Acme SDK-3.0.0"          (string)
result["release-notes"]; // rendered markdown          (string)
result["changelog-entry"]; // one string per release   (string[], from `<multiple:>`)

A <multiple:> block can also declare <name:> to key each item by a computed name instead of array position:

<changelog-by-version>
<multiple: entry in releases>
<name: entry.version>

### {{ entry.version }} — {{ entry.date }}
result["changelog-by-version"];
// { "2.1.0": "### 2.1.0 — 2026-06-01\n", "2.0.0": "### 2.0.0 — 2026-05-01\n" }

renderTemplate(template, inputs) renders an already-parsed template (from parseFile, or a .mdma import emitted by the Vite plugin) — same semantics as render, minus the parse:

import { parseFile, renderTemplate } from "typescript-mdma";

const template = parseFile(source); // parse once ...
renderTemplate(template, { project: "Acme SDK", version: "3.0.0", date: "2026-07-01" }); // ... render many times

renderFile(path, inputs) reads path as UTF-8 and renders it — equivalent to render(readFileSync(path, "utf-8"), inputs):

import { renderFile } from "typescript-mdma";

const result = renderFile("release-notes.mdma", { project: "Acme SDK", version: "3.0.0", date: "2026-07-01" });

writeOutput(result, outputDir, block?) writes a render()/renderFile() result to .md files. Omit block to write every top-level block; pass a block name to write only that one. A string-valued block becomes {outputDir}/{block}.md; a <multiple:> block becomes a directory {outputDir}/{block}/ with one file per item — {name}.md if the block declared <name:>, otherwise {index}.md. Returns the list of paths written.

import { renderFile, writeOutput } from "typescript-mdma";

const result = renderFile("release-notes.mdma", { ... });
writeOutput(result, "out/");                        // every block
writeOutput(result, "out/", "release-notes");        // just that one

getInputs(source) returns the template's @inputs declarations, and validateInputs(source, inputs) checks an inputs object against them without rendering — returning the resolved inputs (defaults applied) or throwing MissingInputError / MdmaTypeError:

import { getInputs, validateInputs } from "typescript-mdma";

getInputs(source);
// [{ name: "project", type: "string", hasDefault: false, default: null }, ...]

validateInputs(source, { project: "Acme SDK", version: "3.0.0", date: "2026-07-01" });
// resolved inputs, with declared defaults applied

Compile-time typed templates

Generate a foo.d.mdma.ts declaration next to each foo.mdma so TypeScript knows the template's @inputs shape. Imports are then typed as ParsedTemplate<{...}>, and renderTemplate(template, inputs) (or your own code, via the MdmaInputs<typeof prompt> helper) flags mismatched inputs in the IDE and in tsc:

import prompt from "./createZone.mdma";
import { renderTemplate, type MdmaInputs } from "typescript-mdma";

renderTemplate(prompt, { existing_zone_ids: ["a"] }); // checked against @inputs

const promptInputs = (): MdmaInputs<typeof prompt> => ({
  existing_zone_ids: [], // missing/extra/mistyped keys are compile errors
});

Two ways to keep the declaration files fresh:

  • Vite plugin (recommended for apps): validates each imported .mdma file at build time, emits its parsed template as the default export (parsed once at module load, never re-parsed), and regenerates the .d.mdma.ts on change.

    // vite.config.ts
    import mdma from "typescript-mdma/vite";
    
    export default defineConfig({
      plugins: [mdma()], // mdma({ typegen: false }) to only validate + parse
    });

    Pass { outDir, sourceRoot } instead of a boolean to mirror declarations under a separate directory rather than writing them next to their source — e.g. to gitignore generated files instead of committing them:

    plugins: [
      mdma({
        typegen: { outDir: path.resolve(__dirname, "typegen"), sourceRoot: path.resolve(__dirname, "src") },
      }),
    ],

    Pair the same two directories in rootDirs (see below) so allowArbitraryExtensions still finds the mirrored declaration.

  • CLI (for CI / pre-commit): mdma-typegen [--out-dir <dir>] [path ...] scans for .mdma files and writes their declarations, mirrored under <dir> (relative to whichever path argument found them) when --out-dir is given; mdma-typegen --check exits 1 if any declaration is missing or stale.

Consumer tsconfig.json requirements: enable "allowArbitraryExtensions": true (TypeScript ≥ 5.0) so import x from "./foo.mdma" resolves the adjacent foo.d.mdma.ts, and remove any global declare module "*.mdma" wildcard so the generated per-file types win. If declarations are mirrored under a separate directory via outDir, also add "rootDirs": ["src", "typegen"] (the same two directories) so TypeScript treats them as one virtual directory when resolving the sibling declaration. Type mapping: string/number/boolean map to themselves, object to Record<string, unknown>, arrays to T[]; inputs with a default become optional properties.

render() throws one of the errors in the package on failure:

| Error class | Condition | |---|---| | MissingInputError | a required input (no default) was not supplied | | MdmaTypeError | an input's runtime type doesn't match its declared type, or a <name:> expression evaluates to something other than a string/number | | MdmaReferenceError | a forward block reference, or an undefined variable | | FilterError | a filter was applied to a value of the wrong type | | MdmaSyntaxError | the .mdma source doesn't conform to the grammar (including <name:> used without a preceding <multiple:>) | | DuplicateNameError | two items in a <multiple:> block computed the same <name:> value |

All extend MdmaError.

Behavioral notes not obvious from spec.md

  • The blank line conventionally left between one block's content and the next block's header (or EOF) is treated as file formatting, not part of either block's rendered value — it's stripped from both ends of the block body before parsing. This is required for block references ({{ blockname }}) to be safely embeddable inline; otherwise every block value would carry a stray trailing newline from that separator. Blank lines inside a body are preserved exactly as written.
  • Whitespace control ({%-/-%}) is applied per-tag, exactly as written — a conditional branch that renders empty does not retroactively remove surrounding blank-line text unless that text is trimmed by an adjacent -.
  • Accessing a missing property on an object/object[] value (e.g. entry.description when description wasn't set) yields null rather than throwing — objects are untyped maps, so this is normal and is what makes | default(...) useful on them. An undefined root identifier (typo'd variable/block/input name) still throws MdmaReferenceError.
  • default([]) and other array literals ([a, b]) are supported in expressions even though the formal grammar doesn't enumerate an array-literal production — the filter reference relies on this syntax ({{ list | default([]) }}).
  • This implementation is a from-scratch parser/interpreter (no Jinja/Liquid dependency), matching the Python package behavior exactly — both share the same golden-output test fixtures.
  • multiple is a reserved word (can't be used as a block or input name), but name is not — <name:> is only ever recognized in its fixed position right after <multiple:>, so an input or block literally named name (e.g. name: string) is unaffected.

Development

npm install
npm run typecheck
npm test
npm run build