Skip to content

Conversation

@sunker
Copy link
Contributor

@sunker sunker commented Feb 5, 2026

What this PR does / why we need it:

This PR introduces a local development server for the new @grafana/plugin-docs-renderer package, enabling plugin developers to preview their multi-page documentation locally before publishing/submitting to the catalog. The server uses the plugin-docs-renderer lib to generate GH flavoured markdown and serves pages based on a manifest.json structure. The server has optional live reload functionality through file watching. Also adding a couple of simple api tests for the express server.

Also replacing the manually edited manifest.json file with an auto-generated file. There's a new scanner file that uses globby to recursively find all markdown files inside the docs folder. It does some light validation, but much more validation will be added in the next PR.

The following things will be addressed in upcoming PRs:

  • navigation - currently renders a flat, ugly-lookfing list.
  • html - the basic layout file will be replaced by something that looks similar to the plugin page in the catalog
  • styling - no proper css theming yet. grafana website styling will be added later on
  • validation - a bunch of validation steps, inc manifest schema verificaiton, will be added in follow-up prs
  • can also look into more advanced live-reload functionality using vite or web-workers

Example usage:

npx @grafana/plugin-docs-renderer ./docs --reload

Screenshot 2026-02-05 at 08 28 08

Which issue(s) this PR fixes:

Part of the rich plugin documentation initiative. This implements the local preview workflow described in the design doc, allowing plugin authors to iterate on documentation with immediate visual feedback.

Fixes https://github.com/grafana/grafana-community-team/issues/739

Special notes for your reviewer:

@grafana-plugins-platform-bot grafana-plugins-platform-bot bot moved this from 📬 Triage to 🔬 In review in Grafana Catalog Team Feb 5, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

Hello! 👋 This repository uses Auto for releasing packages using PR labels.

✨ This PR can be merged. It will not be considered when calculating future versions of the npm packages and will not appear in the changelogs.

@sunker sunker self-assigned this Feb 5, 2026
@sunker sunker added the no-changelog Don't include in changelog and version calculations label Feb 5, 2026
@sunker sunker marked this pull request as ready for review February 5, 2026 08:30
@sunker sunker requested review from a team as code owners February 5, 2026 08:30
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a local development server for the @grafana/plugin-docs-renderer package, enabling plugin developers to preview multi-page documentation locally with optional live reload functionality. The server uses Express to serve GitHub-flavored markdown content based on a manifest.json structure.

Changes:

  • Added Express-based development server with file watching and live reload capabilities
  • Implemented recursive page lookup supporting nested documentation structures
  • Added comprehensive test suite using supertest to validate server functionality

Reviewed changes

Copilot reviewed 8 out of 11 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
packages/plugin-docs-renderer/src/server.ts New Express server implementation with route handlers, file watching, and HTML generation
packages/plugin-docs-renderer/src/server.test.ts Test suite covering server routes, static assets, live reload, and error cases
packages/plugin-docs-renderer/src/index.ts Exports server functions and types for public API
packages/plugin-docs-renderer/src/bin/run.ts CLI completely rewritten to start development server instead of processing docs
packages/plugin-docs-renderer/src/__fixtures__/test-docs/* Test fixtures including manifest, markdown files, and static assets
packages/plugin-docs-renderer/package.json Added express, chokidar, supertest dependencies and dev script
package-lock.json Lock file updates for new dependencies

@@ -0,0 +1,23 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am assuming this will be generated from the docs when we actually run the renderer?

In that case we should have the renderer generate the manifest on the fly instead of having it here hardcoded.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely! To prevent this pr from becoming massive I'll handle that in a follow-up, ok?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to push code that scans the docs folder and generates a manifest based on the filesystem structure + markdown metadata after all. Proper validation will be added in a follow up pr.

`
: '';

return `
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a fan of having an html template here. there are some minimal template engines that we can use for express js and we are going to eventually have to generate more and more htlm that we won't want to write here in JS files

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1d583f9. Went with EJS only because I've used it before.

@sunker sunker requested a review from academo February 5, 2026 21:12
return;
}

debug('Request for slug: %s', slug);

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.

Copilot Autofix

AI about 16 hours ago

To fix the problem, sanitize the user-controlled slug before logging it. For plain-text logging, the key step is to remove newline and carriage-return characters (and optionally other non-printable control characters) so an attacker cannot create extra log lines or otherwise break log structure. We should perform this sanitization only for the log message, leaving the actual routing logic unchanged so functionality is preserved.

The best minimal fix here is to introduce a sanitized variant of slug right before logging: e.g. const safeSlugForLog = slug.replace(/[\r\n]/g, ''); and then pass safeSlugForLog to the debug calls that log the slug. This avoids changing how slug is used to look up pages, but ensures log output does not contain embedded newlines. We do not need extra libraries: a simple String.prototype.replace with a small regex is sufficient. Concretely:

  • In packages/plugin-docs-renderer/src/server/server.ts, within the route handler (app.get('*', ...)), after computing slug and before the debug('Request for slug: %s', slug); call, compute safeSlugForLog by stripping \r and \n.
  • Use safeSlugForLog instead of slug in that debug call, and in the later debug('Page not found for slug: %s', ...) call as well (since that also logs the slug).
  • No new imports, methods, or types are required.
Suggested changeset 1
packages/plugin-docs-renderer/src/server/server.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/plugin-docs-renderer/src/server/server.ts b/packages/plugin-docs-renderer/src/server/server.ts
--- a/packages/plugin-docs-renderer/src/server/server.ts
+++ b/packages/plugin-docs-renderer/src/server/server.ts
@@ -122,12 +122,14 @@
         return;
       }
 
-      debug('Request for slug: %s', slug);
+      // Sanitize slug for logging to prevent log injection via newlines
+      const safeSlugForLog = slug.replace(/[\r\n]/g, '');
+      debug('Request for slug: %s', safeSlugForLog);
 
       // find the file for this slug
       const fileName = findPageBySlug(slug, manifest.pages);
       if (!fileName) {
-        debug('Page not found for slug: %s', slug);
+        debug('Page not found for slug: %s', safeSlugForLog);
         res.status(404).send('Page not found');
         return;
       }
EOF
@@ -122,12 +122,14 @@
return;
}

debug('Request for slug: %s', slug);
// Sanitize slug for logging to prevent log injection via newlines
const safeSlugForLog = slug.replace(/[\r\n]/g, '');
debug('Request for slug: %s', safeSlugForLog);

// find the file for this slug
const fileName = findPageBySlug(slug, manifest.pages);
if (!fileName) {
debug('Page not found for slug: %s', slug);
debug('Page not found for slug: %s', safeSlugForLog);
res.status(404).send('Page not found');
return;
}
Copilot is powered by AI and may make mistakes. Always verify output.
// find the file for this slug
const fileName = findPageBySlug(slug, manifest.pages);
if (!fileName) {
debug('Page not found for slug: %s', slug);

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.

Copilot Autofix

AI about 16 hours ago

To fix this class of problem, any user-controlled value that is written to logs should be sanitized so that it cannot inject new log entries or otherwise break the log format. For plain-text logs, the main concern is removing newline (\n) and carriage return (\r) characters (and optionally other control characters). The safest and least intrusive fix here is to create a small helper that normalizes/sanitizes strings before they are logged and apply it to the slug (and any similar user-derived values) used in logging statements.

Concretely for this file, we can add a local utility function, for example sanitizeForLog, near the top of the file. This helper will take a string (or unknown) and return a string with \r and \n removed (you can also strip other control chars if desired, but we’ll keep the minimal change). Then, on line 130, instead of passing slug directly into debug, we pass sanitizeForLog(slug). This preserves all existing behavior except that line breaks in the slug will be removed before being logged. No external dependencies are required; we can implement the sanitizer with String.prototype.replace. We will keep the rest of the logic (including how slugs are used to look up pages) unchanged to avoid altering runtime behavior outside logging.

Suggested changeset 1
packages/plugin-docs-renderer/src/server/server.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/plugin-docs-renderer/src/server/server.ts b/packages/plugin-docs-renderer/src/server/server.ts
--- a/packages/plugin-docs-renderer/src/server/server.ts
+++ b/packages/plugin-docs-renderer/src/server/server.ts
@@ -12,6 +12,12 @@
 
 const debug = createDebug('plugin-docs-renderer:server');
 
+function sanitizeForLog(value: unknown): string {
+  const str = String(value);
+  // Remove newline and carriage return characters to prevent log injection
+  return str.replace(/[\r\n]+/g, '');
+}
+
 export interface ServerOptions {
   docsPath: string;
   port: number;
@@ -127,7 +133,7 @@
       // find the file for this slug
       const fileName = findPageBySlug(slug, manifest.pages);
       if (!fileName) {
-        debug('Page not found for slug: %s', slug);
+        debug('Page not found for slug: %s', sanitizeForLog(slug));
         res.status(404).send('Page not found');
         return;
       }
EOF
@@ -12,6 +12,12 @@

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

function sanitizeForLog(value: unknown): string {
const str = String(value);
// Remove newline and carriage return characters to prevent log injection
return str.replace(/[\r\n]+/g, '');
}

export interface ServerOptions {
docsPath: string;
port: number;
@@ -127,7 +133,7 @@
// find the file for this slug
const fileName = findPageBySlug(slug, manifest.pages);
if (!fileName) {
debug('Page not found for slug: %s', slug);
debug('Page not found for slug: %s', sanitizeForLog(slug));
res.status(404).send('Page not found');
return;
}
Copilot is powered by AI and may make mistakes. Always verify output.
@sunker sunker force-pushed the docs-parser/local-preview branch from e7fbf3d to c5910db Compare February 10, 2026 09:39
expect(children[1].title).toBe('Database');
});

it('should store nested files with relative paths', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should have a test case for a markdown file with broken or malformed metadata.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 added


// read and parse the file
const fileContent = await readFile(absolutePath, 'utf-8');
const parsed = matter(fileContent);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can matter throw an error if it fails to parse?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep invoking matter on malformed yaml throws. captured in unit tests now.


// validate frontmatter has required fields
const frontmatter = parsed.data as Partial<Frontmatter>;
if (!frontmatter.title || !frontmatter.description || frontmatter.sidebar_position === undefined) {
Copy link
Contributor

@academo academo Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we making sidebar_position mandatory? IMO it should be optional and we just order on whatever order the come from the file system if not defined.

also not 100% sure why description is a mandatory field. do we display it in the sidebar?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM to make description optional and fallback on fs order

description it's not used right now in the preview server, but for SEO reasons I think we need a meta description for the actual website.

// validate frontmatter has required fields
const frontmatter = parsed.data as Partial<Frontmatter>;
if (!frontmatter.title || !frontmatter.description || frontmatter.sidebar_position === undefined) {
console.warn(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is almost silently failing. we should be more aggressive on the feedback that the markdowns file are being ignored

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a future PR coming soon, all validation rules will be added. those will run before the first thing in the serve command so we'll provide clear user feedback then

Copy link
Contributor

@academo academo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my main concerns:

  • review what we should actually make mandatory in the markdown frontmatter
  • better error handling, things like scanDocsFolder are called without a try/catch that could show us a readable error of what's failing
  • add test cases for error states (e.g. corrupted markdown, no files, etc..)

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 23 changed files in this pull request and generated 10 comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog Don't include in changelog and version calculations

Projects

Status: 🔬 In review

Development

Successfully merging this pull request may close these issues.

2 participants