From 3aed223760e1b2d6ba8556dc08e48147ccde0fe1 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 3 Feb 2026 11:00:47 +0100 Subject: [PATCH 01/42] add basic skeleton for new package --- package-lock.json | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 2bed69856e..be8692f7d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39851,10 +39851,17 @@ }, "packages/plugin-docs-renderer": { "name": "@grafana/plugin-docs-renderer", +<<<<<<< HEAD "version": "0.0.1", "license": "Apache-2.0", "engines": { "node": ">=24" +======= + "version": "0.1.0", + "license": "Apache-2.0", + "engines": { + "node": ">=20" +>>>>>>> 63d7ddbf (add basic skeleton for new package) } }, "packages/plugin-e2e": { diff --git a/package.json b/package.json index 1b8e867a49..5cc7b9d192 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@grafana/plugin-tools", - "version": "1.0.0", + "version": "0.0.1", "repository": "https://github.com/grafana/plugin-tools", "author": "Grafana", "private": true, From d2bdfea1d1bb2afe8d822db8929408372565661e Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 3 Feb 2026 14:49:17 +0100 Subject: [PATCH 02/42] revert changes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5cc7b9d192..1b8e867a49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@grafana/plugin-tools", - "version": "0.0.1", + "version": "1.0.0", "repository": "https://github.com/grafana/plugin-tools", "author": "Grafana", "private": true, From 0037f12bc6762e957e3749a143ade988a4aca0a2 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 3 Feb 2026 14:50:08 +0100 Subject: [PATCH 03/42] update lock file after version change --- package-lock.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/package-lock.json b/package-lock.json index be8692f7d1..5cac4f9f60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39851,6 +39851,7 @@ }, "packages/plugin-docs-renderer": { "name": "@grafana/plugin-docs-renderer", +<<<<<<< HEAD <<<<<<< HEAD "version": "0.0.1", "license": "Apache-2.0", @@ -39862,6 +39863,12 @@ "engines": { "node": ">=20" >>>>>>> 63d7ddbf (add basic skeleton for new package) +======= + "version": "0.0.1", + "license": "Apache-2.0", + "engines": { + "node": ">=24" +>>>>>>> 0def04cd (update lock file after version change) } }, "packages/plugin-e2e": { From 9295528387809a815fbbf073b9f5d495b68b9394 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 3 Feb 2026 17:02:52 +0100 Subject: [PATCH 04/42] add deps --- package-lock.json | 16 ++++++++++++++++ packages/plugin-docs-renderer/package.json | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/package-lock.json b/package-lock.json index 5cac4f9f60..4e5da57c9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39855,6 +39855,10 @@ <<<<<<< HEAD "version": "0.0.1", "license": "Apache-2.0", + "dependencies": { + "gray-matter": "^4.0.3", + "marked": "^13.0.0" + }, "engines": { "node": ">=24" ======= @@ -39871,6 +39875,18 @@ >>>>>>> 0def04cd (update lock file after version change) } }, + "packages/plugin-docs-renderer/node_modules/marked": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz", + "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "packages/plugin-e2e": { "name": "@grafana/plugin-e2e", "version": "3.2.1", diff --git a/packages/plugin-docs-renderer/package.json b/packages/plugin-docs-renderer/package.json index 7f1a5a9e38..cf868912a0 100644 --- a/packages/plugin-docs-renderer/package.json +++ b/packages/plugin-docs-renderer/package.json @@ -43,5 +43,9 @@ }, "engines": { "node": ">=24" + }, + "dependencies": { + "marked": "^13.0.0", + "gray-matter": "^4.0.3" } } From 1844749aa5c13f2c6b68d219b02e53ae9a766b5c Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 3 Feb 2026 17:04:46 +0100 Subject: [PATCH 05/42] add basic parser with frontmatter support --- packages/plugin-docs-renderer/src/parser.ts | 42 +++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 packages/plugin-docs-renderer/src/parser.ts diff --git a/packages/plugin-docs-renderer/src/parser.ts b/packages/plugin-docs-renderer/src/parser.ts new file mode 100644 index 0000000000..7a20e52a22 --- /dev/null +++ b/packages/plugin-docs-renderer/src/parser.ts @@ -0,0 +1,42 @@ +import { marked } from 'marked'; +import matter from 'gray-matter'; + +/** + * Result of parsing a markdown file. + */ +export interface ParsedMarkdown { + /** + * Frontmatter metadata extracted from the markdown file. + */ + frontmatter: Record; + + /** + * The rendered HTML content. + */ + html: string; +} + +/** + * Parses markdown content and extracts frontmatter. + * + * @param content - The raw markdown content to parse + * @returns The parsed result with frontmatter and HTML + */ +export function parseMarkdown(content: string): ParsedMarkdown { + // extract frontmatter using gray-matter + const { data: frontmatter, content: markdownContent } = matter(content); + + // parse markdown to HTML using marked + // configure marked to use GitHub Flavored Markdown + marked.setOptions({ + gfm: true, // enable GitHub Flavored Markdown + breaks: false, // don't convert \n to
+ }); + + const html = marked.parse(markdownContent) as string; + + return { + frontmatter, + html, + }; +} From a1e8957704234a15499b864076b8a530073c6ab0 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 3 Feb 2026 17:04:52 +0100 Subject: [PATCH 06/42] add tests --- .../plugin-docs-renderer/src/parser.test.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 packages/plugin-docs-renderer/src/parser.test.ts diff --git a/packages/plugin-docs-renderer/src/parser.test.ts b/packages/plugin-docs-renderer/src/parser.test.ts new file mode 100644 index 0000000000..f65c9c3a99 --- /dev/null +++ b/packages/plugin-docs-renderer/src/parser.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from 'vitest'; +import { parseMarkdown } from './parser.js'; + +describe('parseMarkdown', () => { + it('should parse simple markdown to HTML', () => { + const markdown = '# Hello World\n\nThis is a paragraph.'; + const result = parseMarkdown(markdown); + + expect(result.html).toContain('

Hello World

'); + expect(result.html).toContain('

This is a paragraph.

'); + expect(result.frontmatter).toEqual({}); + }); + + it('should extract frontmatter', () => { + const markdown = `--- +title: Test Page +description: A test page +--- +# Content + +Body text here.`; + + const result = parseMarkdown(markdown); + + expect(result.frontmatter).toEqual({ + title: 'Test Page', + description: 'A test page', + }); + expect(result.html).toContain('

Content

'); + expect(result.html).toContain('

Body text here.

'); + }); + + it('should handle GitHub Flavored Markdown tables', () => { + const markdown = `| Header 1 | Header 2 | +|----------|----------| +| Cell 1 | Cell 2 |`; + + const result = parseMarkdown(markdown); + + expect(result.html).toContain(''); + expect(result.html).toContain(''); + expect(result.html).toContain(''); + }); + + it('should handle code blocks', () => { + const markdown = '```typescript\nconst x = 42;\n```'; + const result = parseMarkdown(markdown); + + expect(result.html).toContain(' { + const markdown = `--- +--- +# Title`; + + const result = parseMarkdown(markdown); + + expect(result.frontmatter).toEqual({}); + expect(result.html).toContain('

Title

'); + }); +}); From 6e75331a221bd69b3a3898dfc1c06de65d3f35e9 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 3 Feb 2026 17:06:11 +0100 Subject: [PATCH 07/42] add loader --- packages/plugin-docs-renderer/src/loader.ts | 74 +++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 packages/plugin-docs-renderer/src/loader.ts diff --git a/packages/plugin-docs-renderer/src/loader.ts b/packages/plugin-docs-renderer/src/loader.ts new file mode 100644 index 0000000000..49828a960e --- /dev/null +++ b/packages/plugin-docs-renderer/src/loader.ts @@ -0,0 +1,74 @@ +import { readFile, readdir } from 'node:fs/promises'; +import { join, relative } from 'node:path'; +import type { Manifest, MarkdownFiles } from './types.js'; + +/** + * Result of loading a docs folder. + */ +export interface LoadedDocs { + /** + * The parsed manifest.json. + */ + manifest: Manifest; + + /** + * All markdown files indexed by their relative path. + */ + files: MarkdownFiles; +} + +/** + * Recursively finds all markdown files in a directory. + * + * @param dir - The directory to search + * @param baseDir - The base directory for calculating relative paths + * @returns Array of absolute file paths + */ +async function findMarkdownFiles(dir: string, baseDir: string): Promise { + const files: string[] = []; + const entries = await readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + + if (entry.isDirectory()) { + // recursively search subdirectories + const subFiles = await findMarkdownFiles(fullPath, baseDir); + files.push(...subFiles); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + files.push(fullPath); + } + } + + return files; +} + +/** + * Loads a documentation folder containing manifest.json and markdown files. + * + * @param rootPath - The absolute path to the docs folder + * @returns The loaded manifest and markdown files + * @throws {Error} If manifest.json is not found or invalid + */ +export async function loadDocsFolder(rootPath: string): Promise { + // read and parse manifest.json + const manifestPath = join(rootPath, 'manifest.json'); + const manifestContent = await readFile(manifestPath, 'utf-8'); + const manifest = JSON.parse(manifestContent) as Manifest; + + // find all markdown files + const markdownPaths = await findMarkdownFiles(rootPath, rootPath); + + // read all markdown files + const files: MarkdownFiles = {}; + for (const filePath of markdownPaths) { + const relativePath = relative(rootPath, filePath); + const content = await readFile(filePath, 'utf-8'); + files[relativePath] = content; + } + + return { + manifest, + files, + }; +} From cd5d8ebb647ca72232ca5d6cf85abd7987451960 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 3 Feb 2026 17:06:18 +0100 Subject: [PATCH 08/42] add root export file --- packages/plugin-docs-renderer/src/index.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/plugin-docs-renderer/src/index.ts b/packages/plugin-docs-renderer/src/index.ts index bece668170..5a73e29cf6 100644 --- a/packages/plugin-docs-renderer/src/index.ts +++ b/packages/plugin-docs-renderer/src/index.ts @@ -1,8 +1,10 @@ -/** - * @grafana/plugin-docs-renderer - * - * A library for rendering Grafana plugin documentation from markdown files. - */ - -// Export types +// export types export type { Manifest, Page, MarkdownFiles } from './types.js'; + +// export parser functions +export { parseMarkdown } from './parser.js'; +export type { ParsedMarkdown } from './parser.js'; + +// export loader functions +export { loadDocsFolder } from './loader.js'; +export type { LoadedDocs } from './loader.js'; From 0c593af653b0995e938349616e1938054adc8507 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 4 Feb 2026 07:31:42 +0100 Subject: [PATCH 09/42] fix broken lock file --- package-lock.json | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e5da57c9f..210cca468c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39851,8 +39851,6 @@ }, "packages/plugin-docs-renderer": { "name": "@grafana/plugin-docs-renderer", -<<<<<<< HEAD -<<<<<<< HEAD "version": "0.0.1", "license": "Apache-2.0", "dependencies": { @@ -39861,18 +39859,6 @@ }, "engines": { "node": ">=24" -======= - "version": "0.1.0", - "license": "Apache-2.0", - "engines": { - "node": ">=20" ->>>>>>> 63d7ddbf (add basic skeleton for new package) -======= - "version": "0.0.1", - "license": "Apache-2.0", - "engines": { - "node": ">=24" ->>>>>>> 0def04cd (update lock file after version change) } }, "packages/plugin-docs-renderer/node_modules/marked": { From 7d43e555311d2717f0b2652ff4efa1eae3b36c13 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 4 Feb 2026 07:32:12 +0100 Subject: [PATCH 10/42] add run script for rendering documentation from markdown files --- packages/plugin-docs-renderer/package.json | 1 + packages/plugin-docs-renderer/src/bin/run.ts | 65 ++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 packages/plugin-docs-renderer/src/bin/run.ts diff --git a/packages/plugin-docs-renderer/package.json b/packages/plugin-docs-renderer/package.json index cf868912a0..7ef7ef87e8 100644 --- a/packages/plugin-docs-renderer/package.json +++ b/packages/plugin-docs-renderer/package.json @@ -6,6 +6,7 @@ "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", + "bin": "./dist/bin/run.js", "files": [ "dist", "LICENSE", diff --git a/packages/plugin-docs-renderer/src/bin/run.ts b/packages/plugin-docs-renderer/src/bin/run.ts new file mode 100644 index 0000000000..6d73714245 --- /dev/null +++ b/packages/plugin-docs-renderer/src/bin/run.ts @@ -0,0 +1,65 @@ +#!/usr/bin/env node +import { access, writeFile } from 'node:fs/promises'; +import { loadDocsFolder } from '../loader.js'; +import { parseMarkdown } from '../parser.js'; + +async function main() { + const docsPath = process.argv[2]; + + // find --output flag + const outputIndex = process.argv.indexOf('--output'); + const outputPath = outputIndex !== -1 ? process.argv[outputIndex + 1] : undefined; + + // check if the first argument is present + if (docsPath === undefined) { + console.error('Please provide the path to the docs folder as an argument.'); + process.exit(1); + } + + // check if --output flag is provided + if (outputPath === undefined) { + console.error('Please provide an output file using the --output flag.'); + process.exit(1); + } + + // check if the path exists + try { + await access(docsPath); + } catch { + console.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; html: string }> = {}; + for (const [filePath, content] of Object.entries(files)) { + const parsed = parseMarkdown(content); + pages[filePath] = { + frontmatter: parsed.frontmatter, + html: parsed.html, + }; + } + + // write output to file + const result = { + manifest, + pages, + }; + + await writeFile(outputPath, JSON.stringify(result, null, 2), 'utf-8'); + console.log(`Documentation rendered successfully to ${outputPath}`); + } catch (error) { + if (error instanceof Error) { + console.error('Error processing documentation:', error.message); + } else { + console.error('Error processing documentation:', error); + } + process.exit(1); + } +} + +main(); From 64e805a0d93e2dbb8aaa955077af3c91806ba1ef Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 4 Feb 2026 07:40:04 +0100 Subject: [PATCH 11/42] add minimist for argument parsing and update package dependencies --- package-lock.json | 9 +++++- packages/plugin-docs-renderer/package.json | 6 +++- packages/plugin-docs-renderer/src/bin/run.ts | 30 ++++++-------------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 210cca468c..cc7708052d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39855,7 +39855,14 @@ "license": "Apache-2.0", "dependencies": { "gray-matter": "^4.0.3", - "marked": "^13.0.0" + "marked": "^13.0.0", + "minimist": "^1.2.8" + }, + "bin": { + "plugin-docs-renderer": "dist/bin/run.js" + }, + "devDependencies": { + "@types/minimist": "^1.2.5" }, "engines": { "node": ">=24" diff --git a/packages/plugin-docs-renderer/package.json b/packages/plugin-docs-renderer/package.json index 7ef7ef87e8..f3f808e6b9 100644 --- a/packages/plugin-docs-renderer/package.json +++ b/packages/plugin-docs-renderer/package.json @@ -47,6 +47,10 @@ }, "dependencies": { "marked": "^13.0.0", - "gray-matter": "^4.0.3" + "gray-matter": "^4.0.3", + "minimist": "^1.2.8" + }, + "devDependencies": { + "@types/minimist": "^1.2.5" } } diff --git a/packages/plugin-docs-renderer/src/bin/run.ts b/packages/plugin-docs-renderer/src/bin/run.ts index 6d73714245..5504e298e7 100644 --- a/packages/plugin-docs-renderer/src/bin/run.ts +++ b/packages/plugin-docs-renderer/src/bin/run.ts @@ -1,32 +1,21 @@ #!/usr/bin/env node -import { access, writeFile } from 'node:fs/promises'; +import { access } from 'node:fs/promises'; +import minimist from 'minimist'; import { loadDocsFolder } from '../loader.js'; import { parseMarkdown } from '../parser.js'; async function main() { - const docsPath = process.argv[2]; - - // find --output flag - const outputIndex = process.argv.indexOf('--output'); - const outputPath = outputIndex !== -1 ? process.argv[outputIndex + 1] : undefined; - - // check if the first argument is present - if (docsPath === undefined) { - console.error('Please provide the path to the docs folder as an argument.'); - process.exit(1); - } - - // check if --output flag is provided - if (outputPath === undefined) { - console.error('Please provide an output file using the --output flag.'); - process.exit(1); - } + const argv = minimist(process.argv.slice(2)); + // get docs path from command line arguments or use default (./docs) + const docsPath = argv._[0] || './docs'; // 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'); process.exit(1); } @@ -44,14 +33,13 @@ async function main() { }; } - // write output to file + // output to console const result = { manifest, pages, }; - await writeFile(outputPath, JSON.stringify(result, null, 2), 'utf-8'); - console.log(`Documentation rendered successfully to ${outputPath}`); + console.log(JSON.stringify(result, null, 2)); } catch (error) { if (error instanceof Error) { console.error('Error processing documentation:', error.message); From 4f2d76bd021fe6dda579c68cd483e57e4379e029 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 4 Feb 2026 09:20:17 +0100 Subject: [PATCH 12/42] add dev script --- packages/plugin-docs-renderer/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/plugin-docs-renderer/package.json b/packages/plugin-docs-renderer/package.json index f3f808e6b9..2eeba65b6f 100644 --- a/packages/plugin-docs-renderer/package.json +++ b/packages/plugin-docs-renderer/package.json @@ -26,6 +26,7 @@ }, "scripts": { "build": "rollup -c ../../rollup.config.ts --configPlugin esbuild", + "dev": "rollup -c ../../rollup.config.ts --configPlugin esbuild --watch", "lint": "eslint --cache ./src", "lint:fix": "npm run lint -- --fix", "lint:package": "publint", From d778359dd12e7121120738efb37b7f613adfc2ec Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 4 Feb 2026 09:37:35 +0100 Subject: [PATCH 13/42] add deps --- package-lock.json | 3 +++ packages/plugin-docs-renderer/package.json | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index cc7708052d..39fb02b418 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39854,6 +39854,8 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { + "chokidar": "^3.6.0", + "express": "^4.18.0", "gray-matter": "^4.0.3", "marked": "^13.0.0", "minimist": "^1.2.8" @@ -39862,6 +39864,7 @@ "plugin-docs-renderer": "dist/bin/run.js" }, "devDependencies": { + "@types/express": "^4.17.21", "@types/minimist": "^1.2.5" }, "engines": { diff --git a/packages/plugin-docs-renderer/package.json b/packages/plugin-docs-renderer/package.json index 2eeba65b6f..44c1b59ee5 100644 --- a/packages/plugin-docs-renderer/package.json +++ b/packages/plugin-docs-renderer/package.json @@ -49,9 +49,12 @@ "dependencies": { "marked": "^13.0.0", "gray-matter": "^4.0.3", - "minimist": "^1.2.8" + "minimist": "^1.2.8", + "express": "^4.18.0", + "chokidar": "^3.6.0" }, "devDependencies": { - "@types/minimist": "^1.2.5" + "@types/minimist": "^1.2.5", + "@types/express": "^4.17.21" } } From b14814fb6223432cebfa5b4e87348ea9046f5f0b Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 4 Feb 2026 09:45:09 +0100 Subject: [PATCH 14/42] add serve cmd --- packages/plugin-docs-renderer/src/bin/run.ts | 77 +++++++++++--------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/packages/plugin-docs-renderer/src/bin/run.ts b/packages/plugin-docs-renderer/src/bin/run.ts index 5504e298e7..1b74d3a6b9 100644 --- a/packages/plugin-docs-renderer/src/bin/run.ts +++ b/packages/plugin-docs-renderer/src/bin/run.ts @@ -1,53 +1,60 @@ #!/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 { startServer } from '../server.js'; + +async function commandServe(docsPath: string, port: number, liveReload: boolean) { + console.log('Starting development server...\n'); + + try { + startServer({ + docsPath, + port, + liveReload, + }); + } catch (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'; + 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._[1] || './docs'); // 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; 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) || port < 1 || port > 65535) { + console.error(`Error: Invalid port: ${argv.port}`); process.exit(1); } + + await commandServe(docsPath, port, argv.reload); } main(); From 8980947f875b6236ebef081b411a403e8523accc Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 4 Feb 2026 09:46:20 +0100 Subject: [PATCH 15/42] add very basic dev server --- packages/plugin-docs-renderer/src/index.ts | 4 + packages/plugin-docs-renderer/src/server.ts | 172 ++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 packages/plugin-docs-renderer/src/server.ts diff --git a/packages/plugin-docs-renderer/src/index.ts b/packages/plugin-docs-renderer/src/index.ts index 5a73e29cf6..f88cd2e52d 100644 --- a/packages/plugin-docs-renderer/src/index.ts +++ b/packages/plugin-docs-renderer/src/index.ts @@ -8,3 +8,7 @@ export type { ParsedMarkdown } from './parser.js'; // export loader functions export { loadDocsFolder } from './loader.js'; export type { LoadedDocs } from './loader.js'; + +// export server functions +export { startServer } from './server.js'; +export type { ServerOptions } from './server.js'; diff --git a/packages/plugin-docs-renderer/src/server.ts b/packages/plugin-docs-renderer/src/server.ts new file mode 100644 index 0000000000..3ec96fee3f --- /dev/null +++ b/packages/plugin-docs-renderer/src/server.ts @@ -0,0 +1,172 @@ +import express, { type Express, type Request, type Response } from 'express'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { watch } from 'chokidar'; +import { parseMarkdown } from './parser.js'; +import type { Manifest, Page } from './types.js'; + +export interface ServerOptions { + docsPath: string; + port?: number; + liveReload?: boolean; +} + +function generateNavItems(pages: Page[]): string { + return pages.map((page) => `
  • ${page.title}
  • `).join('\n'); +} + +/** + * Generates an HTML template for a documentation page. + */ +function generatePageHTML(title: string, content: string, manifest: Manifest, liveReload: boolean): string { + // generate a simple navigation from manifest + const navItems = generateNavItems(manifest.pages); + + // optional live reload script + const reloadScript = liveReload + ? ` + + ` + : ''; + + return ` + + + + + + ${title} - ${manifest.title} + + + +
    +
    + ${content} +
    + ${reloadScript} + + + `.trim(); +} + +/** + * Starts a development server for previewing plugin documentation. + * + * @param options - Server configuration options + * @returns The Express app instance + */ +export function startServer(options: ServerOptions): Express { + const { docsPath, port = 3000, liveReload = false } = options; + + const app = express(); + let lastModified = Date.now(); + + // setup file watcher + const watcher = watch([join(docsPath, '**/*.md'), join(docsPath, 'manifest.json')], { + ignoreInitial: true, + }); + + watcher.on('change', async (path) => { + console.log(`File changed: ${path}`); + lastModified = Date.now(); + console.log('āœ“ Change detected'); + }); + + // serve static assets (images, etc.) + app.use('/img', express.static(join(docsPath, 'img'))); + app.use('/assets', express.static(join(docsPath, 'assets'))); + app.use('/images', express.static(join(docsPath, 'images'))); + + // live reload endpoint (if enabled) + if (liveReload) { + app.get('/__reload__', (req: Request, res: Response) => { + const clientTime = parseInt(req.query.t as string, 10) || 0; + if (lastModified > clientTime) { + res.status(205).send(); // 205 = Reset Content (signals reload) + } else { + res.status(204).send(); // 204 = No Content (no changes) + } + }); + } + + // helper to find page by slug in manifest + function findPageBySlug(slug: string, pages: Page[]): string | null { + for (const page of pages) { + if (page.slug === slug) { + return page.file; + } + if (page.children) { + const found = findPageBySlug(slug, page.children); + if (found) { + return found; + } + } + } + return null; + } + + // serve documentation pages + app.get('/:slug?', async (req: Request, res: Response) => { + try { + // load manifest from disk + const manifestPath = join(docsPath, 'manifest.json'); + const manifestContent = await readFile(manifestPath, 'utf-8'); + const manifest: Manifest = JSON.parse(manifestContent); + + // default to first page in manifest + const slug = req.params.slug || manifest.pages[0]?.slug; + if (!slug) { + res.status(404).send('No pages found in manifest'); + return; + } + + // find the file for this slug + const fileName = findPageBySlug(slug, manifest.pages); + if (!fileName) { + res.status(404).send('Page not found'); + return; + } + + // read and parse the markdown file + const filePath = join(docsPath, fileName); + const fileContent = await readFile(filePath, 'utf-8'); + const parsed = parseMarkdown(fileContent); + + const title = (parsed.frontmatter.title as string) || slug; + const html = generatePageHTML(title, parsed.html, manifest, liveReload); + res.send(html); + } catch (error) { + console.error('Error serving page:', error); + res.status(500).send('Internal server error'); + } + }); + + // start the server + app.listen(port, () => { + console.log(`\nšŸ“„ Plugin Documentation Server`); + console.log(`āœ“ Serving: ${docsPath}`); + console.log(`āœ“ URL: http://localhost:${port}`); + console.log(`āœ“ Live reload: ${liveReload ? 'enabled' : 'disabled'}`); + console.log(`\nšŸ” Watching for changes...\n`); + }); + + return app; +} From 21d71eb731e66002f7659ce34f176b5269e71b2d Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 5 Feb 2026 07:54:27 +0100 Subject: [PATCH 16/42] fix argument parsing --- packages/plugin-docs-renderer/src/bin/run.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-docs-renderer/src/bin/run.ts b/packages/plugin-docs-renderer/src/bin/run.ts index 1b74d3a6b9..edcdab4a19 100644 --- a/packages/plugin-docs-renderer/src/bin/run.ts +++ b/packages/plugin-docs-renderer/src/bin/run.ts @@ -37,7 +37,7 @@ async function main() { }, }); - const docsPath = resolve(argv._[1] || './docs'); + const docsPath = resolve(argv._[0] || './docs'); // check if the path exists try { From bc6dc404413f9ec7a6ee848155adf175ddd3ffe0 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 5 Feb 2026 08:10:45 +0100 Subject: [PATCH 17/42] add server/api test --- package-lock.json | 200 +++++++++++++++++- packages/plugin-docs-renderer/package.json | 4 +- .../src/__fixtures__/test-docs/advanced.md | 3 + .../src/__fixtures__/test-docs/guide.md | 7 + .../src/__fixtures__/test-docs/home.md | 8 + .../src/__fixtures__/test-docs/img/test.png | 1 + .../src/__fixtures__/test-docs/manifest.json | 23 ++ .../plugin-docs-renderer/src/server.test.ts | 130 ++++++++++++ 8 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 packages/plugin-docs-renderer/src/__fixtures__/test-docs/advanced.md create mode 100644 packages/plugin-docs-renderer/src/__fixtures__/test-docs/guide.md create mode 100644 packages/plugin-docs-renderer/src/__fixtures__/test-docs/home.md create mode 100644 packages/plugin-docs-renderer/src/__fixtures__/test-docs/img/test.png create mode 100644 packages/plugin-docs-renderer/src/__fixtures__/test-docs/manifest.json create mode 100644 packages/plugin-docs-renderer/src/server.test.ts diff --git a/package-lock.json b/package-lock.json index 39fb02b418..cc7d5317a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7844,6 +7844,19 @@ "@tybys/wasm-util": "^0.9.0" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -9362,6 +9375,16 @@ "node": ">=14" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@petamoriken/float16": { "version": "3.9.1", "dev": true, @@ -11726,6 +11749,13 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/d3-color": { "version": "3.1.3", "dev": true, @@ -11936,6 +11966,13 @@ "version": "2.0.13", "license": "MIT" }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -12134,6 +12171,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/supports-color": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", @@ -13769,6 +13830,13 @@ "node": ">=0.10.0" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/ast-types": { "version": "0.13.4", "license": "MIT", @@ -15541,6 +15609,16 @@ "dot-prop": "^5.1.0" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -15995,6 +16073,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cookiejs": { "version": "2.1.3", "license": "MIT", @@ -17235,6 +17320,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -19041,6 +19137,13 @@ "version": "2.0.6", "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-shallow-equal": { "version": "1.0.0", "dev": true @@ -19431,6 +19534,24 @@ "node": ">=12.20.0" } }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -35999,6 +36120,81 @@ "dev": true, "license": "MIT" }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "license": "MIT", @@ -39865,7 +40061,9 @@ }, "devDependencies": { "@types/express": "^4.17.21", - "@types/minimist": "^1.2.5" + "@types/minimist": "^1.2.5", + "@types/supertest": "^6.0.2", + "supertest": "^7.0.0" }, "engines": { "node": ">=24" diff --git a/packages/plugin-docs-renderer/package.json b/packages/plugin-docs-renderer/package.json index 44c1b59ee5..9852d5a89e 100644 --- a/packages/plugin-docs-renderer/package.json +++ b/packages/plugin-docs-renderer/package.json @@ -55,6 +55,8 @@ }, "devDependencies": { "@types/minimist": "^1.2.5", - "@types/express": "^4.17.21" + "@types/express": "^4.17.21", + "supertest": "^7.0.0", + "@types/supertest": "^6.0.2" } } diff --git a/packages/plugin-docs-renderer/src/__fixtures__/test-docs/advanced.md b/packages/plugin-docs-renderer/src/__fixtures__/test-docs/advanced.md new file mode 100644 index 0000000000..9652de2cc8 --- /dev/null +++ b/packages/plugin-docs-renderer/src/__fixtures__/test-docs/advanced.md @@ -0,0 +1,3 @@ +# Advanced Topics + +This covers advanced topics. diff --git a/packages/plugin-docs-renderer/src/__fixtures__/test-docs/guide.md b/packages/plugin-docs-renderer/src/__fixtures__/test-docs/guide.md new file mode 100644 index 0000000000..069356d512 --- /dev/null +++ b/packages/plugin-docs-renderer/src/__fixtures__/test-docs/guide.md @@ -0,0 +1,7 @@ +--- +title: User Guide +--- + +# User Guide + +This is a guide page. diff --git a/packages/plugin-docs-renderer/src/__fixtures__/test-docs/home.md b/packages/plugin-docs-renderer/src/__fixtures__/test-docs/home.md new file mode 100644 index 0000000000..afe70d0eaf --- /dev/null +++ b/packages/plugin-docs-renderer/src/__fixtures__/test-docs/home.md @@ -0,0 +1,8 @@ +--- +title: Home Page +description: Welcome to the test docs +--- + +# Welcome + +This is the home page of the test documentation. diff --git a/packages/plugin-docs-renderer/src/__fixtures__/test-docs/img/test.png b/packages/plugin-docs-renderer/src/__fixtures__/test-docs/img/test.png new file mode 100644 index 0000000000..cd69803d6a --- /dev/null +++ b/packages/plugin-docs-renderer/src/__fixtures__/test-docs/img/test.png @@ -0,0 +1 @@ +test-image diff --git a/packages/plugin-docs-renderer/src/__fixtures__/test-docs/manifest.json b/packages/plugin-docs-renderer/src/__fixtures__/test-docs/manifest.json new file mode 100644 index 0000000000..fe31a9bd5c --- /dev/null +++ b/packages/plugin-docs-renderer/src/__fixtures__/test-docs/manifest.json @@ -0,0 +1,23 @@ +{ + "version": "1.0", + "title": "Test Plugin Documentation", + "pages": [ + { + "title": "Home", + "slug": "home", + "file": "home.md" + }, + { + "title": "Guide", + "slug": "guide", + "file": "guide.md", + "children": [ + { + "title": "Advanced", + "slug": "advanced", + "file": "advanced.md" + } + ] + } + ] +} diff --git a/packages/plugin-docs-renderer/src/server.test.ts b/packages/plugin-docs-renderer/src/server.test.ts new file mode 100644 index 0000000000..95283b31e0 --- /dev/null +++ b/packages/plugin-docs-renderer/src/server.test.ts @@ -0,0 +1,130 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import request from 'supertest'; +import { join } from 'node:path'; +import type { Express } from 'express'; +import { startServer } from './server.js'; + +describe('startServer', () => { + const testDocsPath = join(__dirname, '__fixtures__', 'test-docs'); + let app: Express; + + afterEach(() => { + // cleanup: close any open connections + if (app) { + const server = (app as any).server; + if (server?.close) { + server.close(); + } + } + }); + + it('should serve the homepage (first page in manifest)', async () => { + app = startServer({ docsPath: testDocsPath, port: 0 }); + + const response = await request(app).get('/'); + + expect(response.status).toBe(200); + expect(response.text).toContain('Home Page - Test Plugin Documentation'); + expect(response.text).toContain('

    Welcome

    '); + expect(response.text).toContain('This is the home page of the test documentation.'); + }); + + it('should serve a specific page by slug', async () => { + app = startServer({ docsPath: testDocsPath, port: 0 }); + + const response = await request(app).get('/guide'); + + expect(response.status).toBe(200); + expect(response.text).toContain('User Guide - Test Plugin Documentation'); + expect(response.text).toContain('

    User Guide

    '); + expect(response.text).toContain('This is a guide page.'); + }); + + it('should serve a nested page by slug', async () => { + app = startServer({ docsPath: testDocsPath, port: 0 }); + + const response = await request(app).get('/advanced'); + + expect(response.status).toBe(200); + expect(response.text).toContain('

    Advanced Topics

    '); + expect(response.text).toContain('This covers advanced topics.'); + }); + + it('should return 404 for non-existent page', async () => { + app = startServer({ docsPath: testDocsPath, port: 0 }); + + const response = await request(app).get('/non-existent'); + + expect(response.status).toBe(404); + expect(response.text).toContain('Page not found'); + }); + + it('should include navigation links', async () => { + app = startServer({ docsPath: testDocsPath, port: 0 }); + + const response = await request(app).get('/'); + + expect(response.status).toBe(200); + expect(response.text).toContain('
    Header 1Cell 1