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

@markdown-di/bun

v0.11.0

Published

Bun loader for markdown-di - import .md prompt files as typed, strict render functions

Readme

@markdown-di/bun

Bundler-style imports for markdown-di files in the Bun runtime: importing a .md file gives you a typed, strict, synchronous render function — the way webpack loaders turn CSS into modules, but for frontmatter-driven markdown templates.

import compileBrief from './prompts/compile-brief.md'

// compileBrief is a typed render function; its params come from the file's frontmatter.
const prompt = compileBrief({ transcript })

Built for runtime template consumers — e.g. a Bun-run TypeScript CLI that keeps its LLM prompts as .md files with frontmatter and renders them at the moment of use — as a replacement for core's build-oriented BatchProcessor.

Setup

bun add @markdown-di/bun

Register the loader in bunfig.toml so every .md / .markdown import goes through it (this also applies under bun test):

preload = ["@markdown-di/bun/plugin"]

Declaring params

A prompt file declares its render-time inputs in a params: frontmatter block — name: type, with a ? suffix for optional params. The type vocabulary is deliberately small: string, number, boolean, string[], number[], boolean[].

---
name: compile-brief
description: Compile a product brief from an interview transcript
params:
  transcript: string
  productName: string
  attempt?: number
partials:
  guidelines: partials/guidelines.md
---

# Compile a brief for {{productName}}

{{partials.guidelines}}

## Transcript

{{transcript}}

{{#attempt}}
This is attempt {{attempt}} — address the gaps flagged in the previous review.
{{/attempt}}

Core's $dynamic marker also works (transcript: $dynamic declares a required, untyped param), but the params: block is preferred: it reads as a signature at the top of the prompt and carries types for codegen.

Everything else is standard markdown-di: params and frontmatter data fields are one mustache view, partials transclude with {{partials.key}} (globs supported), and partial frontmatter can reach the parent scope with $parent / $parent('key'). Partial paths resolve against the imported file's directory.

Shared partials root

Relative partial paths are jailed to the importing file's directory — .. traversal is rejected by design, so sibling template folders can't reach a common fragment. For fragments shared across folders, declare a partials root in a .markdown-di.json placed at (or above) your templates, and reference it with a ~/ prefix:

{ "partialsRoot": "src/prompts/partials" }
---
partials:
  guidelines: ~/guidelines.md
  snippets: ~/snippets/*.md
---

Resolution rules:

  • The config is discovered by walking up from the imported file's directory; the nearest .markdown-di.json wins and is a boundary — if it declares no partialsRoot, ~/ paths are an error (invalid-declaration), never a fall-through to an outer config.
  • partialsRoot resolves relative to the config file's own directory.
  • The root is a jail like the file-local base: ~/../x and absolute paths are rejected.
  • Globs work under the root; rendering semantics ($parent scoping, nesting, strictness) are identical to file-local partials. A shared partial may itself declare ~/ partials — the root, like the relative base, is anchored at the entry file.
  • A malformed config or a non-string partialsRoot fails loudly.

Strict rendering

The render function throws (a RenderError with a code) instead of ever producing a silently-empty tag:

| violation | code | | --- | --- | | param passed that is not declared in frontmatter | unknown-param | | declared required param missing | missing-param | | param value doesn't match its declared type | wrong-type | | any {{tag}} in the body or a transcluded partial that resolves to nothing (undeclared, null/undefined, or a blank string) | unresolved-tag | | $parent reference the parent scope can't satisfy | unresolved-tag | | native mustache partial {{> x}} | unsupported-tag | | partial path that matches no file | partial-not-found | | circular partial inclusion | circular-partial |

Sections are the escape hatch for optional data: {{#attempt}}…{{/attempt}} over an absent optional param renders nothing by design and is allowed; a bare {{attempt}} interpolation of that absent param throws. Section and inverted-section names must still be declared — a typo in {{#atempt}} throws rather than silently skipping the block.

The same escape hatch extends through transclusion: a partial whose blank render comes from its own conditional sections (e.g. its whole body is {{#note}}…{{/note}}) may be transcluded while blank — that is control flow, not a silent bug. A statically empty partial file still throws unresolved-tag when transcluded.

Checks run against the mustache parse tree (Mustache.parse) before rendering, with mustache's own context-stack lookup semantics, so array/object sections are verified per element.

Module shape

Importing x.md yields:

| export | value | | --- | --- | | default | (params?) => string — the strict render function (output is trimmed, frontmatter is not included) | | frontmatter | the file's parsed frontmatter (including the params: block) | | source | the raw file contents |

Typed imports

Bun plugins can't teach tsc types, so ship declarations with typegen — like typed-css-modules, it emits a sibling declaration per file (compile-brief.d.md.ts for compile-brief.md):

bunx markdown-di-typegen "prompts/**/*.md"
// prompts/compile-brief.d.md.ts (generated)
export interface CompileBriefParams {
  transcript: string
  productName: string
  attempt?: number
}

declare function render(params: CompileBriefParams): string
export default render

export declare const frontmatter: Record<string, unknown>
export declare const source: string

TypeScript picks these up with "allowArbitraryExtensions": true in the consumer's tsconfig. $dynamic params are typed unknown; files with only optional params get an optional params? argument; files with none get render(): string.

For files without a generated declaration, reference the ambient fallback once (files with a sibling .d.md.ts still win):

/// <reference types="@markdown-di/bun/md-modules" />

which types any *.md import as (params?: Record<string, unknown>) => string.

Single-file mode

With many prompts, one sibling .d.md.ts per template clutters the tree. --single-file emits one declaration file instead, containing a wildcard ambient module block per template:

bunx markdown-di-typegen "prompts/**/*.md" --single-file types/prompts.d.ts
// types/prompts.d.ts (generated)
declare module '*compile-brief.md' {
  export interface CompileBriefParams {
    transcript: string
    productName: string
    attempt?: number
  }

  const render: (params: CompileBriefParams) => string
  export default render

  export const frontmatter: Record<string, unknown>
  export const source: string
}

The blocks match import specifiers by filename (./prompts/compile-brief.md matches '*compile-brief.md'), so no sibling files and no allowArbitraryExtensions — the one file just needs to be inside your tsconfig's include. Prefer it when a growing prompt directory makes generated siblings obnoxious; prefer sibling mode when basenames aren't under your control.

Because matching is by filename, single-file mode enforces two constraints and fails loudly (listing the offenders) when they're violated:

  • Basenames must be unique across the globbed templates — a/compile.md and b/compile.md would both match '*compile.md'.
  • No basename may be a proper suffix of another — an import of ./self-narrate.md also matches '*narrate.md'.

Two caveats:

  • Stale siblings shadow the single file. TypeScript prefers a resolved sibling .d.md.ts over ambient wildcard modules, so a leftover sibling from an earlier sibling-mode run silently overrides the single file with possibly stale types. Typegen warns and lists them; delete them when switching modes.
  • Don't combine with the md-modules reference — use --include-fallback instead. Ambient wildcard ties ('*narrate.md' vs '*.md') are broken by declaration order, so a separately loaded generic fallback can shadow the per-template blocks. --include-fallback appends the generic *.md / *.markdown blocks after the per-template blocks in the same file, where they safely lose the tie.

Typegen is also available programmatically:

import { typegen } from '@markdown-di/bun'

typegen('prompts/**/*.md', { cwd: import.meta.dir })

// Single-file mode, with the generic fallback appended:
typegen('prompts/**/*.md', {
  cwd: import.meta.dir,
  singleFile: 'types/prompts.d.ts',
  includeFallback: true,
})

Programmatic rendering

The loader is a thin wrapper over createRenderer, which you can use directly:

import { createRenderer } from '@markdown-di/bun'

const { render, frontmatter, params } = createRenderer('prompts/compile-brief.md')
render({ transcript: '…', productName: 'Jig' })

Bundling and standalone binaries (bun build)

The default markdownDiLoader uses Bun's object loader — it returns a live render function, which is perfect for bun run / bun test but cannot be bundled: a bundler can't serialize a function, so under bun build (and bun build --compile) the export silently collapses to a plain object and calling it throws … is not a function at runtime.

For bun build, use markdownDiBundleLoader instead. It captures each template and every partial it reaches into a self-contained snapshot at build time and emits real JS that rebuilds the render function from that snapshot — no filesystem, no preload, no cwd dependence. It works in a --compile standalone binary and keeps the same exports (default / frontmatter / source) and strict rendering semantics.

// build.ts — note: run this WITHOUT the runtime plugin preloaded, so only the
// bundle loader handles .md (a preloaded markdownDiLoader would compete for it).
import { markdownDiBundleLoader } from '@markdown-di/bun'

await Bun.build({
  entrypoints: ['./src/cli.ts'],
  plugins: [markdownDiBundleLoader],
  compile: { outfile: 'dist/app' }, // omit `compile` for a plain JS bundle
})

The building blocks are also exported directly: collectSources(path) returns a TemplateSnapshot, and createRendererFromSnapshot(snapshot) turns one back into a Renderer — the same pair the bundle loader wires together.

Programmatic rendering

The loader is a thin wrapper over createRenderer, which you can use directly:

import { createRenderer } from '@markdown-di/bun'

const { render, frontmatter, params } = createRenderer('prompts/compile-brief.md')
render({ transcript: '…', productName: 'Jig' })

Semantics and caveats

  • Rendering mirrors @markdown-di/core's processor (partials, nested partials, glob patterns, $parent scoping, unescaped output) and is pinned against core by parity tests — but it is implemented here, synchronously, so render() returns a string, not a Promise.
  • Output is the rendered body only (trimmed); frontmatter and core's output-frontmatter reassembly are a build-pipeline concern and don't apply here.
  • Files without frontmatter import verbatim and declare no params.
  • Custom mustache delimiters and core's onBeforeCompile/variants/schema-validation hooks are not supported through the loader; use core's APIs for build pipelines.
  • Bun-only: the loader uses Bun.plugin. For bun run / bun test, preload markdownDiLoader (via @markdown-di/bun/plugin); for bun build, use markdownDiBundleLoader (see Bundling).