A TypeScript library for managing Markdown-based documentation with schema validation, file operations, and multiple storage backends.
- Read, write, and manage Markdown files with FrontMatter metadata
- Type-safe schema definition with Zod validation
- Support for relations between documents
- Multiple file system backends (Node.js, GitHub, JSON, Mock)
- Archive/restore functionality for documents
- File tree generation
bun add @interactive-inc/docs-client
# or
npm install @interactive-inc/docs-clientimport { DocClient } from "@interactive-inc/docs-client"
import { DocFileSystemNode } from "@interactive-inc/docs-client/file-system-node"
const fileSystem = new DocFileSystemNode({ basePath: "./docs" })
const client = new DocClient({ fileSystem })
// Read a markdown file
const fileRef = client.mdFile("articles/hello.md")
const entity = await fileRef.read()
if (!(entity instanceof Error)) {
console.log(entity.content().title)
console.log(entity.content().body)
}The main entry point for interacting with documents.
const client = new DocClient({
fileSystem,
config: {
defaultIndexIcon: "📁",
indexFileName: "index.md",
archiveDirectoryName: "_",
defaultDirectoryName: "Directory",
indexMetaIncludes: [],
directoryExcludes: [".git"],
metaFileName: ".meta.json",
},
})References are handles to files or directories. They do not load content until you call read().
// File reference
const fileRef = client.mdFile("articles/hello.md")
// Directory reference
const dirRef = client.directory("articles")
// Index file reference
const indexRef = client.indexFile("articles")Entities are immutable objects containing file content and metadata.
const entity = await fileRef.read()
if (!(entity instanceof Error)) {
// Access content
const title = entity.content().title
const body = entity.content().body
const description = entity.content().description
// Access metadata (FrontMatter)
const meta = entity.content().meta()
// Create modified copy (immutable)
const updated = entity.withTitle("New Title")
}// Read a single file
const fileRef = client.mdFile("articles/hello.md")
const entity = await fileRef.read()
// Read all files in a directory
const dirRef = client.directory("articles")
const entities = await dirRef.readMdFiles()
// Read file as raw text
const text = await fileRef.readText()// Write an entity
const entity = fileRef.empty()
const updated = entity
.withTitle("Hello World")
.withDescription("My first article")
await fileRef.write(updated)
// Write raw text
await fileRef.writeText("# Hello\n\nContent here")
// Create a new file with default content
await dirRef.createMdFile("new-article.md")Files can be moved to an archive directory (default: _/).
// Archive a file (moves to _/ subdirectory)
const archivedRef = await fileRef.archive()
// Restore from archive
const restoredRef = await archivedRef.restore()const exists = await fileRef.exists()
const size = await fileRef.size()
const lastModified = await fileRef.lastModified()
const createdAt = await fileRef.createdAt()Define schemas to validate and type FrontMatter metadata.
Create a .meta.json file in any directory:
{
"icon": "📄",
"schema": {
"title": {
"type": "text",
"required": true,
"title": "Title",
"description": "Document title",
"default": null
},
"category": {
"type": "select-text",
"required": false,
"title": "Category",
"description": null,
"options": ["tutorial", "reference", "guide"],
"default": null
},
"tags": {
"type": "multi-text",
"required": false,
"title": "Tags",
"description": null,
"default": []
},
"author": {
"type": "relation",
"required": false,
"title": "Author",
"description": null,
"path": "authors",
"default": null
}
}
}If .meta.json does not exist, schema is read from index.md:
---
icon: 📄
schema:
title:
type: text
required: true
category:
type: select-text
options:
- tutorial
- reference
---
# Articles
Directory description here.Use defineSchema for type-safe schema definitions:
import { defineSchema, docCustomSchemaField } from "@interactive-inc/docs-client"
const articleSchema = defineSchema({
title: { type: "text", required: true },
views: { type: "number", required: false },
published: { type: "boolean", required: true },
category: { type: "select-text", required: false },
tags: { type: "multi-text", required: false },
author: docCustomSchemaField.relation(false, "authors"),
relatedArticles: docCustomSchemaField.multiRelation(false, "articles"),
})
// Use schema with directory
const dirRef = client.directory("articles", articleSchema)
const fileRef = dirRef.mdFile("hello")
const entity = await fileRef.read()
if (!(entity instanceof Error)) {
// Type-safe access to metadata
const title = entity.content().meta().field("title") // string
const views = entity.content().meta().field("views") // number | null
}| Type | Value Type | Description |
|---|---|---|
text |
string |
Single text value |
number |
number |
Numeric value |
boolean |
boolean |
True/false |
select-text |
string |
Single selection from text options |
select-number |
number |
Single selection from number options |
multi-text |
string[] |
Multiple text values |
multi-number |
number[] |
Multiple number values |
multi-select-text |
string[] |
Multiple selections from text options |
multi-select-number |
number[] |
Multiple selections from number options |
relation |
string |
Reference to another document |
multi-relation |
string[] |
References to multiple documents |
Relations link documents to each other.
// Define schema with relations
const pageSchema = defineSchema({
features: docCustomSchemaField.multiRelation(true, "features"),
})
const dirRef = client.directory("pages", pageSchema)
const pageRef = dirRef.mdFile("dashboard")
const page = await pageRef.read()
// Get related documents
const featureRefs = await pageRef.relations("features")
for (const featureRef of featureRefs) {
const feature = await featureRef.read()
console.log(feature.content().title)
}const dirRef = client.directory("articles")
// List contents
const fileNames = await dirRef.fileNames()
const dirNames = await dirRef.directoryNames()
// Get references
const files = await dirRef.files()
const mdFiles = await dirRef.mdFiles()
const subdirs = await dirRef.directories()
// Read all files
const entities = await dirRef.readMdFiles()
// Access index file
const indexEntity = await dirRef.readIndexFile()
// Navigate to subdirectory
const subDirRef = dirRef.directory("tutorials")
// Create new file
const newFileRef = await dirRef.createMdFile("new-article.md")Generate a hierarchical tree structure of documents.
// Get full file tree
const tree = await client.fileTree()
// Get directory-only tree
const dirTree = await client.directoryTree()
// Tree node structure
type DocTreeNode = {
type: "file" | "directory"
name: string
path: string
icon: string
title: string
children?: DocTreeNode[] // Only for directories
}For local file operations:
import { DocFileSystemNode } from "@interactive-inc/docs-client/file-system-node"
const fileSystem = new DocFileSystemNode({ basePath: "./docs" })For reading from GitHub repositories:
import { DocFileSystem } from "@interactive-inc/docs-client/file-system"
import { DocFileSystemOctokitRead } from "@interactive-inc/docs-client/file-system-octokit"
import { DocFileSystemNodeWrite } from "@interactive-inc/docs-client/file-system-node"
const reader = new DocFileSystemOctokitRead({
token: process.env.GITHUB_TOKEN,
owner: "your-org",
repo: "your-repo",
basePath: "docs",
branch: "main",
})
const writer = new DocFileSystemNodeWrite({
basePath: "/tmp/docs-output",
})
const fileSystem = new DocFileSystem({
basePath: "docs",
reader,
writer,
})For in-memory operations with JSON data:
import { DocFileSystemJson } from "@interactive-inc/docs-client/file-system-json"
const fileSystem = new DocFileSystemJson({
basePath: "docs",
files: {
"index.md": "# Home\n\nWelcome!",
"articles/hello.md": "# Hello\n\nContent here.",
},
})For testing:
import { DocFileSystemMock } from "@interactive-inc/docs-client/file-system-mock"
const fileSystem = new DocFileSystemMock({ basePath: "docs" })| Method | Description |
|---|---|
file(path) |
Get reference by path (auto-detects type) |
mdFile(path) |
Get Markdown file reference |
indexFile(path) |
Get index file reference |
directory(path) |
Get directory reference |
fileTree(path?) |
Build file tree from path |
directoryTree(path?) |
Build directory-only tree |
basePath() |
Get base path |
| Method | Description |
|---|---|
read() |
Read file and return entity |
readText() |
Read raw file content |
write(entity) |
Write entity to file |
writeText(text) |
Write raw text to file |
writeDefault() |
Create file with default content |
delete() |
Delete file |
exists() |
Check if file exists |
archive() |
Move to archive directory |
restore() |
Restore from archive |
size() |
Get file size in bytes |
lastModified() |
Get last modified time |
createdAt() |
Get creation time |
directory() |
Get parent directory reference |
relation(key) |
Get single relation reference |
relations(key) |
Get multi-relation references |
| Method | Description |
|---|---|
fileNames() |
List file names |
directoryNames() |
List subdirectory names |
files() |
Get all file references |
mdFiles() |
Get Markdown file references |
directories() |
Get subdirectory references |
file(name) |
Get file reference by name |
mdFile(name) |
Get Markdown file reference |
directory(name) |
Get subdirectory reference |
indexFile() |
Get index file reference |
readFiles() |
Read all files |
readMdFiles() |
Read all Markdown files |
readIndexFile() |
Read index file |
createMdFile(name?) |
Create new Markdown file |
exists() |
Check if directory exists |
| Method | Description |
|---|---|
content() |
Get content value object |
path |
Get path information |
withContent(content) |
Create copy with new content |
withTitle(title) |
Create copy with new title |
withDescription(desc) |
Create copy with new description |
withMeta(meta) |
Create copy with new metadata |
toJson() |
Convert to JSON |
MIT