@markdown-di/cli
v0.3.0
Published
Format markdown files with frontmatter dependency injection
Maintainers
Readme
@markdown-di/cli
Format markdown files with frontmatter dependency injection and TypeScript schema validation.
Installation
# Local installation (recommended)
bun add -D @markdown-di/cli
# Or use with bunx (no install needed)
bunx @markdown-di/cliUsage
Create a markdown-di.config.ts file in your project root:
import { z } from 'zod';
import type { MarkdownDIConfig } from '@markdown-di/cli';
// Define your schemas
const documentSchema = z.object({
name: z.string(),
description: z.string(),
partials: z.record(z.string()).optional()
});
const blogPostSchema = z.object({
name: z.string(),
title: z.string(),
author: z.string(),
date: z.string(),
partials: z.record(z.string()).optional()
});
export default {
baseDir: './docs',
include: ['**/*.md'],
exclude: ['node_modules/**', '.git/**'],
schemas: {
'document': documentSchema,
'blog-post': blogPostSchema
}
} satisfies MarkdownDIConfig;Then run the CLI:
# Format all markdown files
markdown-di
# Or with bunx
bunx @markdown-di/cliConfiguration
The markdown-di.config.ts file accepts the following options:
interface MarkdownDIConfig {
/**
* Base directory containing markdown files to format
* @default process.cwd()
*/
baseDir?: string;
/**
* Glob patterns to match markdown files
* @default ['**/*.md']
*/
include?: string[];
/**
* Glob patterns to exclude
* @default ['node_modules/**', '.git/**']
*/
exclude?: string[];
/**
* Registered schemas for frontmatter validation
* Key is the schema name (matches frontmatter.name field)
* These are registered using mdi.registerSchemas() internally
*/
schemas?: Record<string, ZodSchema>;
/**
* Hook called before compilation to inject variables into frontmatter
* @param context - Hook context with file ID, path, frontmatter, baseDir, and dynamicFields
* @param context.dynamicFields - Array of field names marked as $dynamic in frontmatter
* @returns Object to be deep merged into frontmatter (must include all $dynamic fields)
*/
onBeforeCompile?: (context: HookContext) => Promise<Record<string, unknown>> | Record<string, unknown>;
/**
* Check mode - exits with error if files would change
* @default false
*/
check?: boolean;
}How It Works
Markdown files with frontmatter define dependencies using the partials key:
docs/guide.md:
---
name: document
description: User guide
partials:
intro: sections/intro.md
features: sections/features.md
---
# User Guide
{{partials.intro}}
## Features
{{partials.features}}The {{partials.intro}} syntax gets replaced with the contents of sections/intro.md relative to the base directory.
Schema Validation
Schemas are registered in the config file. The CLI validates frontmatter against the schema that matches the name field:
// In markdown-di.config.ts
schemas: {
'document': documentSchema, // Validates files with name: document
'blog-post': blogPostSchema // Validates files with name: blog-post
}Variable Injection with onBeforeCompile Hook
Inject dynamic variables into frontmatter before compilation using the onBeforeCompile hook. This enables "templates of templates" functionality.
Dynamic Variables with $dynamic
Mark fields as $dynamic in frontmatter to declare that they must be provided by the hook. The build will fail if the hook doesn't provide all declared dynamic fields:
import type { MarkdownDIConfig } from '@markdown-di/cli';
import { z } from 'zod';
const blogPostSchema = z.object({
id: z.string().optional(),
name: z.string(),
title: z.string(),
author: z.string(),
publishedAt: z.string().datetime(),
status: z.enum(['draft', 'published', 'archived'])
});
export default {
baseDir: './blog',
schemas: {
'blog-post': blogPostSchema
},
onBeforeCompile: async (context) => {
// context.id = "blog.posts.intro" (auto-generated from path)
// context.filePath = "/path/to/blog/posts/intro.md"
// context.dynamicFields = ["author", "publishedAt"] (fields marked as $dynamic)
// MUST provide all dynamic fields or build fails
return {
author: await fetchAuthor(context.id),
publishedAt: new Date().toISOString(),
};
}
} satisfies MarkdownDIConfig;Markdown file with $dynamic fields:
---
schema: blog-post
name: Getting Started
title: My First Post
author: $dynamic
publishedAt: $dynamic
status: published
---
# {{title}}
By {{author}} on {{publishedAt}}
Status: {{status}}Key features:
$dynamicdeclaration: Explicitly mark fields that need hook injection- Build-time validation: Fails if hook doesn't provide all
$dynamicfields - Static override: Replace
$dynamicwith a real value to skip hook for that field - File ID: Auto-generated from path (
docs/intro.md→docs.intro) - Execution timing: Runs before schema validation
- Deep merge: Hook results are deep-merged with existing frontmatter
- Schema validation: Merged frontmatter is validated against schema after injection
Error handling:
If the hook doesn't provide a $dynamic field, the build fails:
✗ Found 1 error in blog/post.md:
schema: Hook must provide these $dynamic fields: publishedAt at onBeforeCompileCI/CD Integration
Set check: true in your config or use an environment variable:
# In CI - check without modifying files
CHECK=true markdown-diUpdate your config to read the environment:
export default {
baseDir: './docs',
check: process.env.CHECK === 'true',
schemas: { /* ... */ }
} satisfies MarkdownDIConfig;Exit Codes
0- Success (all files formatted or already formatted)1- Errors found or files would be changed (in check mode)
License
MIT © Pepijn Senders
