Skip to main content

Programmatic API

@readma/core is the rendering engine behind the CLI. Everything the CLI does is composed from exported, testable pieces — so you can integrate Readma into your own tooling.

deno add jsr:@readma/core

One-shot rendering​

readme​

async function readme(
rt: ReadmeTemplateArgs,
options?: Partial<GlobalOptions>,
): Promise<string>

Resolves the context and renders the full readme markdown. No side effects — nothing is written to disk. This is what --dry-run and --stdout use.

writeReadme​

async function writeReadme(
rt: ReadmeTemplateArgs,
options?: Partial<GlobalOptions>,
): Promise<string>

Renders like readme, then writes README.md into the resolved folderPath unless dryRun is set. Returns the rendered markdown.

GlobalOptions​

type GlobalOptions = {
workspaceRootPath?: string // default "."
dryRun?: boolean // skip the file write
folderPath?: string // default: workspace root path
}

Partial renders​

renderHeader / renderFooter​

function renderHeader(ctx: ReadmeContext): string
function renderFooter(ctx: ReadmeContext): string

Header = anchor, logo, title, description, quick links and badges. Footer = repobeats image and the markdown reference links of the badges.

Sections​

type SectionDef = {
id: string
title: string
render: (ctx: ReadmeContext) => string | undefined
}

function allSections(ctx: ReadmeContext): SectionDef[]
function renderSections(
ctx: ReadmeContext,
defs: SectionDef[],
renderSection: (title: string, content: string | undefined) => string,
): string
function installSnippet(
pkgName: string,
language: "rs" | "ts",
packageRegistry: "jsr" | "npm" | "crates.io" | undefined,
): string | undefined

renderSections skips sections whose body is undefined or empty — pass your own renderSection callback to add numbers, back-to-top links, etc.

Context resolution​

resolveContext​

async function resolveContext(
args: ReadmeTemplateArgs,
options?: Partial<GlobalOptions>,
deps?: Partial<ResolveContextDeps>,
): Promise<ReadmeContext>

Resolves every environment-dependent value once — branch, license, code of conduct path, issue template URLs, badges, image URLs — so rendering stays pure. deps lets you inject detectBranch / detectLicense for tests.

The returned ReadmeContext carries args, defaulted options, repoUrl, docUrl, demoUrl, branch, fullEmail, logoSrc, screenshotSrc, license, cocPath, badgeList and issueUrls.

Table of contents​

class Tocer {
constructor(hasBackToTop: boolean, tocConfig?: TocConfig)
anchor(title: string): string // GitHub-compatible, deduplicated
toc(): MdSrc // the <details> block
section(title: string, content: MdSrc | undefined): MdSrc
}

Tocer reproduces GitHub's anchor algorithm:

ghSlug("About the project"); // "about-the-project"
ghSlug("💡 Features"); // "-features"
ghSlug("Contact & support"); // "contact--support"

Duplicate headings get the same -1, -2 suffixes GitHub would produce.

Badges​

function shield(
name: string,
linkUrl: string,
shieldUrl: string,
logo?: string,
): Shield

function shields(args: ShieldsArgs): Shield[]
function renderShields(list: Shield[]): { shieldsBadges: string; shieldsRefs: string }
function shieldUrl(path: string, style?: string, logo?: string): string

shields builds the full badge list from args, honoring badges.disabled and badges.custom. See badges & shields for the behavior; the rendering format is documented there too.

IO and detection​

async function detectLicense(rootPath?: string): Promise<LicenseInfo>
function matchLicense(content: string): string | undefined
async function detectBranch(rootPath?: string): Promise<string>
async function getReadmaConfig(configPathRoot?: string): Promise<ReadmeTemplateArgs>
async function readOptionalFile(filePath: string): Promise<string | undefined>
async function issueTemplateUrl(rootPath: string, repoUrl: string, templateFilename: string): Promise<string>
async function fetchJsonDescription(url: string, offline: boolean, fetchFn?: typeof fetch): Promise<string | undefined>

License detection​

detectLicense probes, in order: LICENSE.txt, LICENSE.md, LICENSE, LICENCE.txt, LICENCE.md, LICENCE, LICENSE-MIT. matchLicense then matches the first 600 characters against the well-known headers: MIT, Apache-2.0, MPL-2.0, GPL-3.0, BSD-3-Clause.

Branch detection​

GITHUB_HEAD_REF, then GITHUB_REF_NAME, then git rev-parse --abbrev-ref HEAD, then "main".

Injectable IO​

type ReadmaIo = {
readTextFile: (filePath: string) => Promise<string>
cwd: () => string
runCommand: (command: { cmd: string; args: string[]; cwd?: string }) => Promise<{ success: boolean; stdout: string; stderr: string }>
fetch: typeof fetch
}

function setIo(overrides: Partial<ReadmaIo>): void
function resetIo(): void

setIo swaps the side-effect surface — used by the test suite to run 100% coverage without real git, network or filesystem access. Use it in your own tests the same way.

MDX​

async function renderMdx(source: string): Promise<string>
async function processFile(path: string): Promise<string | undefined>

Compile an MDX string to static HTML (renderMdx) or read an MDX file and compile it (processFile, returns undefined when the file is missing). Failures throw a typed ReadmaError.

Errors and markdown utils​

class ReadmaError extends Error {}
const md = {
code: (c: string, lng = "sh") => string // fenced code block
link: (text: string, url: string) => string
img: (alt: string, src: string) => string
}

Example: render a readme for CI artifacts​

import { readme, types } from "@readma/core";

const args: types.ReadmeTemplateArgs = {
// …your config, or import it:
// ...(await getReadmaConfig("./config")),
};

const markdown = await readme(args, {
workspaceRootPath: ".",
});

await Deno.writeTextFile("artifacts/README.preview.md", markdown);