Repository: grafana/plugin-tools
Pull request #1770: Docs: auto generate plugin schema reference
Author: login: hugohaggmark
Labels: name: type/docs
description: Changes only affect the documentation
name: no-changelog
description: Don't include in changelog and version calculations
Created: 2025-05-07T04:53:06Z | Merged: 2025-05-08T12:26:51Z

<!--

Thank you for sending a pull request! Here are some tips:

1. If this is your first time, please read our contribution guide at https://github.com/grafana/plugin-tools/blob/main/CONTRIBUTING.md

2. Ensure you include and run the appropriate tests as part of your Pull Request.

3. In a new feature or configuration option, an update to the documentation is necessary. Everything related to the documentation is under the docs folder in the root of the repository.

4. If the Pull Request is a work in progress, make use of GitHub's "Draft PR" feature and mark it as such.

5. If you can not merge your Pull Request due to a merge conflict, Rebase it. This gets it in sync with the main branch.

6. Name your PR as "<FeatureArea>: Describe your change", e.g. Alerting: Prevent race condition. If it's a fix or feature relevant for the changelog describe the user impact in the title. The PR title is used to auto-generate the changelog for issues marked with the "add to changelog" label.

-->

**What this PR does / why we need it**:
This PR downloads the [plugins-schema](https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json) and generates markdown from the schema using [jsonschema2mk](https://github.com/simonwalz/jsonschema2mk).

**LIMITATIONS**
This approach unfortunately comes with a small (or big, depends how you feel) limitation and that is the linking from tables to headers are based on full paths of properties in the json schema. This means that the Docusaurus quick links and headers in the document show the full path to properties. I've tried different approaches by customizing different [`partials`](https://github.com/simonwalz/jsonschema2mk/tree/master/partials) but I think we would need to hook into [extensions](https://github.com/simonwalz/jsonschema2mk/tree/master/extensions) to make the links unique because for example `exposedcomponents` appears twice in the document. 

| Before | After |
|-----|-----|
|![image](https://github.com/user-attachments/assets/8fda921f-ad59-49e3-ad59-43c199436dec)|![image](https://github.com/user-attachments/assets/cd6fec15-8f81-493d-802d-0494f3ab946d)|
|![image](https://github.com/user-attachments/assets/86ac3ccd-f848-4b9c-90ff-6abfcd79524f)|![image](https://github.com/user-attachments/assets/0bc8b1ac-8cf6-47a0-bab8-d7588f6b3337)|




**Which issue(s) this PR fixes**:

<!--

- Automatically closes linked issue when the Pull Request is merged.

Usage: "Fixes #<issue number>", or "Fixes (paste link of issue)"

-->

Fixes #1651 , #1650 

**Special notes for your reviewer**:
Passing runs here https://github.com/grafana/plugin-tools/actions/runs/14875523629/job/41772019976?pr=1770 and here https://github.com/grafana/plugin-tools/actions/runs/14875523629/job/41772019976?pr=1770 look for `Markdown generation complete!` in the `Build documentation website (Dev Portal)` step

Deployed the [latest commit](https://github.com/grafana/plugin-tools/pull/1770/commits/1f7bdfd90e2642c864be8a26a04b7d8227a6acce) to [DEV bucket](https://grafana-dev.com/developers/plugin-tools/reference/plugin-json) so you can test

To test this PR:
1. git clone this repo
2. run `npm ci`
3. run `npm run docs:clear`
4. run `DEV_PORTAL_ENV=dev npm run docs:build`, shouldn't create any build errors ✅ 
5. run `npm run docs`, should render a new Metadata (plugin.json) page that is in sync with plugin schema ✅ 

Linked issue #1651: Docs: Update the representation of the `plugin.json` schema docs
The [representation of the `plugin.json` metadata](https://grafana.com/developers/plugin-tools/reference/plugin-json) on our dev portal is is very hard to oversee.

### Goal
- [ ] Find a representation that is easier to consume by plugin devs
- [ ] Implement the change

Diff:
diff --git a/docusaurus/website/package.json b/docusaurus/website/package.json
index 3df4a66767..beef5f1cf7 100644
--- a/docusaurus/website/package.json
+++ b/docusaurus/website/package.json
@@ -5,6 +5,7 @@
   "scripts": {
     "docusaurus": "docusaurus",
     "start": "docusaurus start",
+    "prebuild": "./scripts/download-schema.sh && ./scripts/generate-markdown.sh",
     "build": "docusaurus build",
     "swizzle": "docusaurus swizzle",
     "deploy": "docusaurus deploy",
diff --git a/docusaurus/website/scripts/download-schema.sh b/docusaurus/website/scripts/download-schema.sh
new file mode 100755
index 0000000000..3e1bc58947
--- /dev/null
+++ b/docusaurus/website/scripts/download-schema.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# Script to download the Grafana plugin schema JSON file
+# and store it locally in the same directory
+
+# Exit on error
+set -e
+
+# Define variables
+SCHEMA_URL="https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json"
+OUTPUT_FILE="$(dirname "$0")/plugin.schema.json"
+
+echo "Downloading Grafana plugin schema..."
+
+# Download the file
+if curl -s -f -o "$OUTPUT_FILE" "$SCHEMA_URL"; then
+    echo "Schema successfully downloaded to $OUTPUT_FILE"
+else
+    echo "Error: Failed to download schema from $SCHEMA_URL" >&2
+    exit 1
+fi
+
+# Verify the downloaded file
+if [ ! -s "$OUTPUT_FILE" ]; then
+    echo "Error: Downloaded file is empty" >&2
+    rm -f "$OUTPUT_FILE"
+    exit 1
+fi
+
+
+echo "Schema download complete!"
diff --git a/docusaurus/website/scripts/generate-markdown.sh b/docusaurus/website/scripts/generate-markdown.sh
new file mode 100755
index 0000000000..e394eda875
--- /dev/null
+++ b/docusaurus/website/scripts/generate-markdown.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+
+# Script generate markdown files from the Grafana plugin schema JSON file
+
+# Exit on error
+set -e
+
+INPUT_FILE="$(dirname "$0")/plugin.schema.json"
+PARTIALS="$(dirname "$0")/partials"
+OUTPUT_FILE="$(dirname "$0")/../../docs/reference/metadata.md"
+
+# Verify the downloaded file
+if [ ! -s "$INPUT_FILE" ]; then
+    echo "Error: input schema doesn't exist or is empty" >&2
+    rm -f "$INPUT_FILE"
+    exit 1
+fi
+
+npx --yes jsonschema2mk --partials "$PARTIALS" --schema "$INPUT_FILE" > "$OUTPUT_FILE"
+
+# Detect OS and set sed options accordingly
+if [[ "$OSTYPE" == "darwin"* ]]; then
+    SED_OPTS="-i ''"
+else
+    SED_OPTS="-i"
+fi
+
+# Add docusaurus header to the top of the file
+sed $SED_OPTS "1i\\
+---\\
+id: plugin-json\\
+title: Metadata (plugin.json)\\
+description: Reference for the Grafana plugin.json metadata file.\\
+keywords:\\
+  - grafana\\
+  - plugins\\
+  - documentation\\
+  - plugin.json\\
+  - API reference\\
+  - API\\
+sidebar_position: 10\\
+---\\
+\\
+# Plugin metadata (plugin.json)
+" "$OUTPUT_FILE"
+
+rm -f "$INPUT_FILE"
+
+echo "Markdown generation complete!"
\ No newline at end of file
diff --git a/docusaurus/website/scripts/partials/element.md b/docusaurus/website/scripts/partials/element.md
new file mode 100644
index 0000000000..2721db1a44
--- /dev/null
+++ b/docusaurus/website/scripts/partials/element.md
@@ -0,0 +1,17 @@
+{{#if path~}}
+<a name="{{{tolink (or path 'root')}}}"></a>
+{{{mdlevel path}}}{{#if path}} {{escape path}}{{/if}}
+{{/if~}}
+{{#if (or description deprecated)}}
+
+{{#if deprecated}}(DEPRECATED) {{/if}}{{{description}}}
+
+{{/if}}
+
+{{> type . ~}}
+{{#each (get_examples .) ~}}
+{{> example ~}}
+{{/each ~}}
+{{#each (get_ref_items) ~}}
+{{>element . ~}}
+{{/each ~}}
diff --git a/docusaurus/website/scripts/partials/example.md b/docusaurus/website/scripts/partials/example.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docusaurus/website/scripts/partials/object.md b/docusaurus/website/scripts/partials/object.md
new file mode 100644
index 0000000000..bfefabf0d6
--- /dev/null
+++ b/docusaurus/website/scripts/partials/object.md
@@ -0,0 +1,41 @@
+{{#if (length properties) ~}}
+**{{prefix_text}}Properties**
+
+{{> object_property_header}}
+{{#each properties ~}}
+{{> object_property (jsmk_property . path=(pathjoinobj ../path @key .) parent=.. name=@key)}}
+{{/each}}
+
+{{/if}}
+{{#if (length patternProperties) ~}}
+**{{prefix_text}}Properties (Pattern)**
+
+{{> object_property_header}}
+{{#each patternProperties ~}}
+{{> object_property (jsmk_property . path=(pathjoinobj ../path @key .) parent=.. name=@key)}}
+{{/each}}
+
+{{/if}}
+
+{{#if (isdefined minProperties)}}
+**{{prefix_text}}Minimal Properties:** {{escape minProperties}}{{br}}
+{{/if~}}
+{{#if (isdefined maxProperties)}}
+**{{prefix_text}}Maximal Properties:** {{escape maxProperties}}{{br}}
+{{/if~}}
+{{#if (and (isdefined propertyNames) (isdefined propertyNames.pattern))}}
+**{{prefix_text}}Property Name Pattern:** {{code (escapeRegexp propertyNames.pattern)}}{{br}}
+{{/if~}}
+{{#if (isdefined dependentRequired)}}
+{{#each dependentRequired}}
+**{{prefix_text}}If property _{{@key}}_ is defined**, property/ies {{#each this}}_{{this}}_{{#unless @last}}, {{/unless}}{{/each}} is/are required.{{br}}
+{{/each}}
+{{/if~}}
+{{#if (isdefined dependentSchemas)}}
+{{#each dependentSchemas}}
+**{{prefix_text}}If property _{{@key}}_ is defined**:
+
+{{> element_part this type=(or type ../type) path=(pathjoin path (plus "dependentSchemas " @key))}}
+
+{{/each}}
+{{/if~}}
diff --git a/docusaurus/website/scripts/partials/object_property.md b/docusaurus/website/scripts/partials/object_property.md
new file mode 100644
index 0000000000..bc9fac5d65
--- /dev/null
+++ b/docusaurus/website/scripts/partials/object_property.md
@@ -0,0 +1,13 @@
+|
+{{~#mylink .}}**{{escape name}}**{{/mylink ~}}
+{{#if (and title (title_isnot_name .))}}<br/>({{escape title}}){{/if ~}}
+|
+{{~code display_type ~}}
+|
+{{~#if deprecated}}(DEPRECATED)<br/>{{/if}}
+{{~#if description ~}}
+{{firstline description .}}<br/>{{/if ~}}
+{{>extra_inline . ~}}
+|
+{{~#if (isdefined required)}}{{#if required}} ✅ {{/if}}{{/if~}}
+|
diff --git a/docusaurus/website/scripts/partials/object_property_header.md b/docusaurus/website/scripts/partials/object_property_header.md
new file mode 100644
index 0000000000..5c37aa3d9f
--- /dev/null
+++ b/docusaurus/website/scripts/partials/object_property_header.md
@@ -0,0 +1,2 @@
+| Name | Type | Description | Required |
+| ---- | ---- | ----------- | :------: |
diff --git a/docusaurus/website/scripts/partials/type.md b/docusaurus/website/scripts/partials/type.md
new file mode 100644
index 0000000000..90bfae3807
--- /dev/null
+++ b/docusaurus/website/scripts/partials/type.md
@@ -0,0 +1,9 @@
+{{#if (is_type type "object") ~}}
+{{> object . ~}}
+{{~else~}}
+{{#if (is_type type "array") ~}}
+{{> array . ~}}
+{{~else~}}
+{{> simple ~}}
+{{/if ~}}
+{{/if ~}}


Reviews:
user: login: hugohaggmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T05:06:46Z
user: login: hugohaggmark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T07:26:11Z
user: login: tolzhabayev
body: 🚀 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-08T12:20:44Z

Review comments:
diff_hunk: @@ -0,0 +1,42 @@
+#!/bin/bash
+
+# Script to download the Grafana plugin schema JSON file
+# and store it locally in the same directory
+
+# Exit on error
+set -e
+
+# Define variables
+SCHEMA_URL="https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json"
+OUTPUT_FILE="$(dirname "$0")/plugin.schema.json"
+
+echo "Downloading Grafana plugin schema..."
+
+# Download the file
+if curl -s -f -o "$OUTPUT_FILE" "$SCHEMA_URL"; then
+    echo "Schema successfully downloaded to $OUTPUT_FILE"
+else
+    echo "Error: Failed to download schema from $SCHEMA_URL" >&2
+    exit 1
+fi
+
+# Verify the downloaded file
+if [ ! -s "$OUTPUT_FILE" ]; then
+    echo "Error: Downloaded file is empty" >&2
+    rm -f "$OUTPUT_FILE"
+    exit 1
+fi
+
+# Detect OS and set sed options accordingly
+if [[ "$OSTYPE" == "darwin"* ]]; then
+    SED_OPTS="-i ''"
+else
+    SED_OPTS="-i"
+fi
+
+# Replace patterns with backticks
+sed $SED_OPTS 's/'\''{PLUGIN_ID}\/name-of-component\/v1'\''/`{PLUGIN_ID}\/name-of-component\/v1`/g' "$OUTPUT_FILE"
+sed $SED_OPTS 's/'\''{PLUGIN_ID}\/name-of-my-extension-point\/v1'\''/`{PLUGIN_ID}\/name-of-my-extension-point\/v1`/g' "$OUTPUT_FILE"
path: docusaurus/website/scripts/download-schema.sh
user: login: hugohaggmark
body: Temporary fix until https://github.com/grafana/grafana/pull/105047 is merged
created_at: 2025-05-07T05:06:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,42 @@
+#!/bin/bash
+
+# Script to download the Grafana plugin schema JSON file
+# and store it locally in the same directory
+
+# Exit on error
+set -e
+
+# Define variables
+SCHEMA_URL="https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json"
+OUTPUT_FILE="$(dirname "$0")/plugin.schema.json"
+
+echo "Downloading Grafana plugin schema..."
+
+# Download the file
+if curl -s -f -o "$OUTPUT_FILE" "$SCHEMA_URL"; then
+    echo "Schema successfully downloaded to $OUTPUT_FILE"
+else
+    echo "Error: Failed to download schema from $SCHEMA_URL" >&2
+    exit 1
+fi
+
+# Verify the downloaded file
+if [ ! -s "$OUTPUT_FILE" ]; then
+    echo "Error: Downloaded file is empty" >&2
+    rm -f "$OUTPUT_FILE"
+    exit 1
+fi
+
+# Detect OS and set sed options accordingly
+if [[ "$OSTYPE" == "darwin"* ]]; then
+    SED_OPTS="-i ''"
+else
+    SED_OPTS="-i"
+fi
+
+# Replace patterns with backticks
+sed $SED_OPTS 's/'\''{PLUGIN_ID}\/name-of-component\/v1'\''/`{PLUGIN_ID}\/name-of-component\/v1`/g' "$OUTPUT_FILE"
+sed $SED_OPTS 's/'\''{PLUGIN_ID}\/name-of-my-extension-point\/v1'\''/`{PLUGIN_ID}\/name-of-my-extension-point\/v1`/g' "$OUTPUT_FILE"
path: docusaurus/website/scripts/download-schema.sh
user: login: hugohaggmark
body: Fixed 👍 
created_at: 2025-05-08T07:26:11Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-07T04:53:22Z
body: <!-- plugin-tools-auto-check-labels-comment -->
Hello! 👋 This repository uses [Auto](https://intuit.github.io/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.
author_association: CONTRIBUTOR
user: login: tolzhabayev
created_at: 2025-05-08T12:16:23Z
body: > Docusaurus quick links and headers in the document show the full path to properties

I actually like it better this way 👍 
author_association: COLLABORATOR