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);