Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3aed223
add basic skeleton for new package
sunker Feb 3, 2026
d2bdfea
revert changes
sunker Feb 3, 2026
0037f12
update lock file after version change
sunker Feb 3, 2026
9295528
add deps
sunker Feb 3, 2026
1844749
add basic parser with frontmatter support
sunker Feb 3, 2026
a1e8957
add tests
sunker Feb 3, 2026
6e75331
add loader
sunker Feb 3, 2026
cd5d8eb
add root export file
sunker Feb 3, 2026
0c593af
fix broken lock file
sunker Feb 4, 2026
7d43e55
add run script for rendering documentation from markdown files
sunker Feb 4, 2026
64e805a
add minimist for argument parsing and update package dependencies
sunker Feb 4, 2026
4f2d76b
add dev script
sunker Feb 4, 2026
d778359
add deps
sunker Feb 4, 2026
b14814f
add serve cmd
sunker Feb 4, 2026
8980947
add very basic dev server
sunker Feb 4, 2026
21d71eb
fix argument parsing
sunker Feb 5, 2026
bc6dc40
add server/api test
sunker Feb 5, 2026
71afcc1
remove process exit
sunker Feb 5, 2026
77816cd
fix image
sunker Feb 5, 2026
c054c9f
Merge main into docs-parser/local-preview
sunker Feb 5, 2026
89f125f
use debug logger in server
sunker Feb 5, 2026
721edf7
cleanup
sunker Feb 5, 2026
078ba67
more cleanup
sunker Feb 5, 2026
9d1773f
escape html
sunker Feb 5, 2026
f60c2d5
remove redundant debug log for requested slug in startServer
sunker Feb 5, 2026
6517241
update startServer to return Server instance with close method
sunker Feb 5, 2026
1d583f9
adding ejs templates
sunker Feb 5, 2026
c1aac11
add copyAssets function to rollup config for static asset handling
sunker Feb 5, 2026
0c66dc1
put devtools inside cli folder
sunker Feb 5, 2026
286cc1a
add Frontmatter interface for markdown file metadata
sunker Feb 5, 2026
507664f
moved loader out of lib and into cli
sunker Feb 5, 2026
eb62cbd
add scanner functionality to process markdown files and generate docu…
sunker Feb 5, 2026
cdad9b1
update startServer to be async and integrate docs folder scanning for…
sunker Feb 6, 2026
a1bbb94
load markdown files into memory and improve manifest logging
sunker Feb 6, 2026
0b2a54b
fix tests and self review
sunker Feb 6, 2026
1bdbecd
add scanner tests
sunker Feb 6, 2026
a81a176
remove ununsed loader file
sunker Feb 6, 2026
e7fbf3d
update tests for nested directory handling
sunker Feb 6, 2026
721445d
add headings extraction to scanned files and update types for table o…
sunker Feb 6, 2026
df2d54c
add styles copying to the build process in rollup configuration
sunker Feb 6, 2026
a1d1d15
add minimal Grafana-themed CSS and postbuild script for documentation…
sunker Feb 6, 2026
6aadc1d
add new layout and navigation for documentation pages
sunker Feb 6, 2026
bd07ef8
add marked-gfm-heading-id extension for automatic heading IDs and upd…
sunker Feb 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 291 additions & 5 deletions package-lock.json

Large diffs are not rendered by default.

25 changes: 20 additions & 5 deletions packages/plugin-docs-renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
},
"scripts": {
"build": "rollup -c ../../rollup.config.ts --configPlugin esbuild",
"postbuild": "cp -r src/server/views dist/server/ && cp -r src/server/styles dist/server/",
"dev": "rollup -c ../../rollup.config.ts --configPlugin esbuild --watch",
"lint": "eslint --cache ./src",
"lint:fix": "npm run lint -- --fix",
"lint:package": "publint",
Expand All @@ -46,15 +48,28 @@
"node": ">=24"
},
"dependencies": {
"marked": "^13.0.0",
"@types/ejs": "^3.1.5",
"chokidar": "^3.6.0",
"debug": "^4.3.7",
"ejs": "^4.0.1",
"express": "^4.18.0",
"fs-extra": "^11.3.3",
"github-slugger": "^1.5.0",
"globby": "^11.1.0",
"gray-matter": "^4.0.3",
"minimist": "^1.2.8",
"isomorphic-dompurify": "^2.16.0",
"debug": "^4.3.7"
"marked": "^13.0.0",
"marked-gfm-heading-id": "^4.1.3",
"minimist": "^1.2.8"
},
"devDependencies": {
"@types/minimist": "^1.2.5",
"@types/debug": "^4.1.12",
"@types/dompurify": "^3.0.5",
"@types/debug": "^4.1.12"
"@types/express": "^4.17.21",
"@types/fs-extra": "^11.0.4",
"@types/github-slugger": "^1.3.0",
"@types/minimist": "^1.2.5",
"@types/supertest": "^6.0.2",
"supertest": "^7.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Advanced Topics
description: Advanced topics for experienced users
sidebar_position: 3
---

# Advanced Topics

This covers advanced topics.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Database
description: Database configuration options
sidebar_position: 2
---

# Database

Configure database connections.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Settings
description: Configuration settings
sidebar_position: 1
---

# Settings

Configure your plugin settings here.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: User Guide
description: A guide page for users
sidebar_position: 2
---

# User Guide

This is a guide page.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Home Page
description: Welcome to the test docs
sidebar_position: 1
---

# Welcome

This is the home page of the test documentation.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 49 additions & 35 deletions packages/plugin-docs-renderer/src/bin/run.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,67 @@
#!/usr/bin/env node
import { access } from 'node:fs/promises';
import { resolve } from 'node:path';
import minimist from 'minimist';
import { loadDocsFolder } from '../loader.js';
import { parseMarkdown } from '../parser.js';
import createDebug from 'debug';
import { startServer } from '../server/server.js';

const debug = createDebug('plugin-docs-renderer:cli');

async function commandServe(docsPath: string, port: number, liveReload: boolean) {
debug('Command serve: docsPath=%s, port=%d, liveReload=%s', docsPath, port, liveReload);

try {
await startServer({
docsPath,
port,
liveReload,
});
} catch (error) {
debug('Failed to start server: %O', error);
if (error instanceof Error) {
console.error('Error starting server:', error.message);
} else {
console.error('Error starting server:', error);
}
process.exit(1);
}
}

async function main() {
const argv = minimist(process.argv.slice(2));
// get docs path from command line arguments or use default (./docs)
const docsPath = argv._[0] || './docs';
debug('CLI invoked with args: %O', process.argv.slice(2));

const argv = minimist(process.argv.slice(2), {
boolean: ['reload'],
string: ['port'],
alias: {
p: 'port',
r: 'reload',
},
default: {
port: '3001',
reload: false,
},
});

const docsPath = resolve(argv._[0] || './docs');
debug('Resolved docs path: %s', docsPath);

// check if the path exists
try {
await access(docsPath);
} catch {
console.error(`Path not found: ${docsPath}`);
console.error('Usage: npx @grafana/plugin-docs-renderer [docs-folder]');
console.error('Example: npx @grafana/plugin-docs-renderer ./docs');
console.error(`Error: Path not found: ${docsPath}`);
process.exit(1);
}

// load and parse documentation
try {
const { manifest, files } = await loadDocsFolder(docsPath);

// parse each markdown file
const pages: Record<string, { frontmatter: Record<string, unknown>; html: string }> = {};
for (const [filePath, content] of Object.entries(files)) {
const parsed = parseMarkdown(content);
pages[filePath] = {
frontmatter: parsed.frontmatter,
html: parsed.html,
};
}

// output to console
const result = {
manifest,
pages,
};

console.log(JSON.stringify(result, null, 2));
} catch (error) {
if (error instanceof Error) {
console.error('Error processing documentation:', error.message);
} else {
console.error('Error processing documentation:', error);
}
// parse port
const port = parseInt(argv.port, 10);
if (isNaN(port)) {
console.error(`Error: Invalid port: ${argv.port}`);
process.exit(1);
}

await commandServe(docsPath, port, argv.reload);
}

main();
121 changes: 121 additions & 0 deletions packages/plugin-docs-renderer/src/cli/scanner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { describe, it, expect } from 'vitest';
import { join } from 'node:path';
import { scanDocsFolder } from './scanner.js';

describe('scanDocsFolder', () => {
const testDocsPath = join(__dirname, '..', '__fixtures__', 'test-docs');

it('should scan markdown files and generate manifest', async () => {
const result = await scanDocsFolder(testDocsPath);

expect(result.manifest).toBeDefined();
expect(result.manifest.title).toBe('Plugin Documentation');
expect(result.manifest.pages).toHaveLength(4); // home, guide, advanced, config
});

it('should sort pages by sidebar_position', async () => {
const result = await scanDocsFolder(testDocsPath);

const pages = result.manifest.pages;
expect(pages[0].title).toBe('Home Page');
expect(pages[0].slug).toBe('home');
expect(pages[1].title).toBe('User Guide');
expect(pages[1].slug).toBe('guide');
expect(pages[2].title).toBe('Advanced Topics');
expect(pages[2].slug).toBe('advanced');
});

it('should generate slugs from file paths', async () => {
const result = await scanDocsFolder(testDocsPath);

const pages = result.manifest.pages;
expect(pages[0].slug).toBe('home');
expect(pages[1].slug).toBe('guide');
expect(pages[2].slug).toBe('advanced');
});

it('should load file contents into memory', async () => {
const result = await scanDocsFolder(testDocsPath);

expect(result.files).toBeDefined();
expect(Object.keys(result.files)).toHaveLength(5); // includes nested config files
expect(result.files['home.md']).toContain('---');
expect(result.files['home.md']).toContain('title: Home Page');
expect(result.files['home.md']).toContain('# Welcome');
});

it('should preserve frontmatter in file contents', async () => {
const result = await scanDocsFolder(testDocsPath);

const homeContent = result.files['home.md'];
expect(homeContent).toContain('title: Home Page');
expect(homeContent).toContain('description: Welcome to the test docs');
expect(homeContent).toContain('sidebar_position: 1');
});

it('should include file reference in page object', async () => {
const result = await scanDocsFolder(testDocsPath);

const pages = result.manifest.pages;
expect(pages[0].file).toBe('home.md');
expect(pages[1].file).toBe('guide.md');
expect(pages[2].file).toBe('advanced.md');
});

it('should throw error when no valid markdown files found', async () => {
const emptyPath = join(__dirname, '..', '__fixtures__', 'non-existent');

await expect(scanDocsFolder(emptyPath)).rejects.toThrow('No valid markdown files found');
});

describe('nested directories', () => {
it('should create category page for directories with files', async () => {
const result = await scanDocsFolder(testDocsPath);

const configPage = result.manifest.pages.find((p) => p.slug === 'config');
expect(configPage).toBeDefined();
expect(configPage?.title).toBe('Config');
expect(configPage?.children).toBeDefined();
expect(configPage?.children).toHaveLength(2);
});

it('should generate slugs with directory prefixes', async () => {
const result = await scanDocsFolder(testDocsPath);

const configPage = result.manifest.pages.find((p) => p.slug === 'config');
const children = configPage?.children || [];

expect(children[0].slug).toBe('config/settings');
expect(children[1].slug).toBe('config/database');
});

it('should sort nested pages by sidebar_position', async () => {
const result = await scanDocsFolder(testDocsPath);

const configPage = result.manifest.pages.find((p) => p.slug === 'config');
const children = configPage?.children || [];

expect(children[0].title).toBe('Settings');
expect(children[1].title).toBe('Database');
});

it('should store nested files with relative paths', async () => {
const result = await scanDocsFolder(testDocsPath);

expect(result.files['config/settings.md']).toBeDefined();
expect(result.files['config/settings.md']).toContain('title: Settings');
expect(result.files['config/database.md']).toBeDefined();
expect(result.files['config/database.md']).toContain('title: Database');
});

it('should reference nested files correctly in page objects', async () => {
const result = await scanDocsFolder(testDocsPath);

const configPage = result.manifest.pages.find((p) => p.slug === 'config');
const children = configPage?.children || [];

expect(children[0].file).toBe('config/settings.md');
expect(children[1].file).toBe('config/database.md');
});
});
});
Loading