@grafana/plugin-docs-cli 0.0.0 → 0.0.10-canary.2537.23286312909.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,206 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join, relative, dirname } from 'node:path';
3
+ import matter from 'gray-matter';
4
+ import { Rule } from '../types.js';
5
+
6
+ const REQUIRED_FIELDS = [
7
+ { key: "title", type: "string" },
8
+ { key: "description", type: "string" }
9
+ ];
10
+ const OPTIONAL_FIELDS = [{ key: "sidebar_position", type: "number" }];
11
+ function isSlugSafe(raw) {
12
+ const trimmed = raw.trim().replace(/^\/+|\/+$/g, "");
13
+ if (!trimmed) {
14
+ return false;
15
+ }
16
+ if (!/^[A-Za-z0-9/_-]+$/.test(trimmed)) {
17
+ return false;
18
+ }
19
+ const segments = trimmed.split("/");
20
+ return !segments.some((s) => !s || s === "." || s === "..");
21
+ }
22
+ function findFieldLine(raw, key) {
23
+ const lines = raw.split("\n");
24
+ for (let i = 1; i < lines.length; i++) {
25
+ if (lines[i].trim() === "---") {
26
+ break;
27
+ }
28
+ if (lines[i].startsWith(`${key}:`)) {
29
+ return i + 1;
30
+ }
31
+ }
32
+ return void 0;
33
+ }
34
+ function findH1Lines(raw) {
35
+ const lines = raw.split("\n");
36
+ let inFrontmatter = false;
37
+ let passedFrontmatter = false;
38
+ const result = [];
39
+ for (let i = 0; i < lines.length; i++) {
40
+ if (lines[i].trim() === "---") {
41
+ if (!inFrontmatter) {
42
+ inFrontmatter = true;
43
+ } else {
44
+ passedFrontmatter = true;
45
+ }
46
+ continue;
47
+ }
48
+ if (passedFrontmatter && /^# /.test(lines[i])) {
49
+ result.push(i + 1);
50
+ }
51
+ }
52
+ return result;
53
+ }
54
+ async function checkFrontmatter(input) {
55
+ const diagnostics = [];
56
+ let entries = [];
57
+ try {
58
+ entries = await readdir(input.docsPath, { recursive: true, withFileTypes: true });
59
+ } catch {
60
+ return diagnostics;
61
+ }
62
+ const mdFiles = entries.filter(
63
+ (e) => e.isFile() && e.name.endsWith(".md") && !e.parentPath.includes("node_modules") && !e.parentPath.includes("dist")
64
+ );
65
+ const records = [];
66
+ for (const file of mdFiles) {
67
+ const absolutePath = join(file.parentPath, file.name);
68
+ const relativePath = relative(input.docsPath, absolutePath);
69
+ let raw;
70
+ try {
71
+ raw = await readFile(absolutePath, "utf-8");
72
+ } catch {
73
+ continue;
74
+ }
75
+ if (!raw.startsWith("---")) {
76
+ diagnostics.push({
77
+ rule: Rule.BlockExists,
78
+ severity: "error",
79
+ file: relativePath,
80
+ line: 1,
81
+ title: "Missing frontmatter block",
82
+ detail: "Every page must start with a frontmatter block (---). Add at least title and description."
83
+ });
84
+ continue;
85
+ }
86
+ let data;
87
+ try {
88
+ ({ data } = matter(raw));
89
+ } catch (err) {
90
+ diagnostics.push({
91
+ rule: Rule.ValidYaml,
92
+ severity: "error",
93
+ file: relativePath,
94
+ line: 1,
95
+ title: "Invalid YAML in frontmatter",
96
+ detail: err instanceof Error ? err.message : "YAML parse error."
97
+ });
98
+ continue;
99
+ }
100
+ for (const { key, type } of REQUIRED_FIELDS) {
101
+ if (!(key in data)) {
102
+ diagnostics.push({
103
+ rule: Rule.RequiredFields,
104
+ severity: "error",
105
+ file: relativePath,
106
+ line: 1,
107
+ title: `Missing required field: ${key}`,
108
+ detail: `Every page needs a "${key}" (${type}) in its frontmatter.`
109
+ });
110
+ } else if (typeof data[key] !== type) {
111
+ diagnostics.push({
112
+ rule: Rule.FieldTypes,
113
+ severity: "error",
114
+ file: relativePath,
115
+ line: findFieldLine(raw, key),
116
+ title: `Wrong type for field: ${key}`,
117
+ detail: `"${key}" should be a ${type} but got ${typeof data[key]}.`
118
+ });
119
+ }
120
+ }
121
+ for (const { key, type } of OPTIONAL_FIELDS) {
122
+ if (key in data && typeof data[key] !== type) {
123
+ diagnostics.push({
124
+ rule: Rule.FieldTypes,
125
+ severity: "error",
126
+ file: relativePath,
127
+ line: findFieldLine(raw, key),
128
+ title: `Wrong type for field: ${key}`,
129
+ detail: `"${key}" should be a ${type} but got ${typeof data[key]}.`
130
+ });
131
+ }
132
+ }
133
+ if ("slug" in data && typeof data.slug === "string" && !isSlugSafe(data.slug)) {
134
+ diagnostics.push({
135
+ rule: Rule.ValidSlug,
136
+ severity: "warning",
137
+ file: relativePath,
138
+ line: findFieldLine(raw, "slug"),
139
+ title: "Invalid custom slug",
140
+ detail: `The slug "${data.slug}" contains unsafe characters. Only letters, digits, underscores, hyphens and forward slashes are allowed.`
141
+ });
142
+ }
143
+ for (const h1Line of findH1Lines(raw)) {
144
+ diagnostics.push({
145
+ rule: Rule.NoH1,
146
+ severity: "warning",
147
+ file: relativePath,
148
+ line: h1Line,
149
+ title: "Avoid h1 headings in content",
150
+ detail: "The page title comes from frontmatter. Any h1 in the body will be stripped by the parser and replaced by the frontmatter title. Use h2 (##) or lower."
151
+ });
152
+ }
153
+ records.push({
154
+ relativePath,
155
+ parentDir: dirname(relativePath),
156
+ sidebarPosition: typeof data.sidebar_position === "number" ? data.sidebar_position : void 0,
157
+ sidebarPositionLine: findFieldLine(raw, "sidebar_position"),
158
+ customSlug: typeof data.slug === "string" && isSlugSafe(data.slug) ? data.slug.trim().replace(/^\/+|\/+$/g, "") : void 0,
159
+ customSlugLine: findFieldLine(raw, "slug")
160
+ });
161
+ }
162
+ const byDir = Map.groupBy(records, (r) => r.parentDir);
163
+ for (const siblings of byDir.values()) {
164
+ const posCounts = /* @__PURE__ */ new Map();
165
+ for (const record of siblings) {
166
+ if (record.sidebarPosition !== void 0) {
167
+ posCounts.set(record.sidebarPosition, (posCounts.get(record.sidebarPosition) ?? 0) + 1);
168
+ }
169
+ }
170
+ for (const record of siblings) {
171
+ if (record.sidebarPosition === void 0 || (posCounts.get(record.sidebarPosition) ?? 0) <= 1) {
172
+ continue;
173
+ }
174
+ diagnostics.push({
175
+ rule: Rule.DuplicatePosition,
176
+ severity: input.strict ? "error" : "warning",
177
+ file: record.relativePath,
178
+ line: record.sidebarPositionLine,
179
+ title: `Duplicate sidebar_position: ${record.sidebarPosition}`,
180
+ detail: `sidebar_position ${record.sidebarPosition} is shared by multiple siblings. Each sibling page must have a unique position.`
181
+ });
182
+ }
183
+ }
184
+ const slugCounts = /* @__PURE__ */ new Map();
185
+ for (const record of records) {
186
+ if (record.customSlug) {
187
+ slugCounts.set(record.customSlug, (slugCounts.get(record.customSlug) ?? 0) + 1);
188
+ }
189
+ }
190
+ for (const record of records) {
191
+ if (!record.customSlug || (slugCounts.get(record.customSlug) ?? 0) <= 1) {
192
+ continue;
193
+ }
194
+ diagnostics.push({
195
+ rule: Rule.DuplicateSlug,
196
+ severity: "error",
197
+ file: record.relativePath,
198
+ line: record.customSlugLine,
199
+ title: `Duplicate slug: "${record.customSlug}"`,
200
+ detail: `Slug "${record.customSlug}" is used by multiple pages. Each page must have a unique URL slug.`
201
+ });
202
+ }
203
+ return diagnostics;
204
+ }
205
+
206
+ export { checkFrontmatter };
@@ -0,0 +1,17 @@
1
+ import { checkAssets } from './assets.js';
2
+ import { checkCrossFile } from './cross-file.js';
3
+ import { checkFilesystem } from './filesystem.js';
4
+ import { checkFrontmatter } from './frontmatter.js';
5
+ import { checkManifest } from './manifest.js';
6
+ import { checkMarkdown } from './markdown.js';
7
+
8
+ const allRules = [
9
+ checkFilesystem,
10
+ checkFrontmatter,
11
+ checkAssets,
12
+ checkMarkdown,
13
+ checkCrossFile,
14
+ checkManifest
15
+ ];
16
+
17
+ export { allRules };
@@ -0,0 +1,105 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { relative, join } from 'node:path';
3
+ import { scanDocsFolder } from '../../scanner.js';
4
+ import { Rule } from '../types.js';
5
+
6
+ function collectPageFiles(pages) {
7
+ const refs = [];
8
+ for (const page of pages) {
9
+ if (page.file) {
10
+ refs.push({ file: page.file, slug: page.slug });
11
+ }
12
+ if (page.children) {
13
+ refs.push(...collectPageFiles(page.children));
14
+ }
15
+ }
16
+ return refs;
17
+ }
18
+ function validatePage(page, path, diagnostics) {
19
+ if (!page.title) {
20
+ diagnostics.push({
21
+ rule: Rule.ManifestValid,
22
+ severity: "error",
23
+ title: "Manifest page missing title",
24
+ detail: `Page at "${path}" has no title.`
25
+ });
26
+ }
27
+ if (!page.slug && page.slug !== "") {
28
+ diagnostics.push({
29
+ rule: Rule.ManifestValid,
30
+ severity: "error",
31
+ title: "Manifest page missing slug",
32
+ detail: `Page "${page.title || path}" has no slug.`
33
+ });
34
+ }
35
+ if (page.children) {
36
+ for (let i = 0; i < page.children.length; i++) {
37
+ validatePage(page.children[i], `${path}/${page.children[i].slug || i}`, diagnostics);
38
+ }
39
+ }
40
+ }
41
+ async function checkManifest(input) {
42
+ const diagnostics = [];
43
+ let manifest;
44
+ try {
45
+ const scanned = await scanDocsFolder(input.docsPath);
46
+ manifest = scanned.manifest;
47
+ } catch {
48
+ return diagnostics;
49
+ }
50
+ if (!manifest.version) {
51
+ diagnostics.push({
52
+ rule: Rule.ManifestValid,
53
+ severity: "error",
54
+ title: "Manifest missing version",
55
+ detail: "The generated manifest has no version field."
56
+ });
57
+ }
58
+ if (!manifest.pages || !Array.isArray(manifest.pages)) {
59
+ diagnostics.push({
60
+ rule: Rule.ManifestValid,
61
+ severity: "error",
62
+ title: "Manifest missing pages",
63
+ detail: "The generated manifest has no pages array."
64
+ });
65
+ return diagnostics;
66
+ }
67
+ if (manifest.pages.length === 0) {
68
+ diagnostics.push({
69
+ rule: Rule.ManifestValid,
70
+ severity: "error",
71
+ title: "Manifest has no pages",
72
+ detail: "The generated manifest has an empty pages array."
73
+ });
74
+ return diagnostics;
75
+ }
76
+ for (const page of manifest.pages) {
77
+ validatePage(page, page.slug || "(root)", diagnostics);
78
+ }
79
+ let entries = [];
80
+ try {
81
+ entries = await readdir(input.docsPath, { recursive: true, withFileTypes: true });
82
+ } catch {
83
+ return diagnostics;
84
+ }
85
+ const allFiles = new Set(
86
+ entries.filter((e) => e.isFile()).map((e) => relative(input.docsPath, join(e.parentPath, e.name)))
87
+ );
88
+ const pageFiles = collectPageFiles(manifest.pages);
89
+ for (const { file, slug } of pageFiles) {
90
+ if (!file) {
91
+ continue;
92
+ }
93
+ if (!allFiles.has(file)) {
94
+ diagnostics.push({
95
+ rule: Rule.ManifestRefsExist,
96
+ severity: "error",
97
+ title: "Manifest references missing file",
98
+ detail: `Page "${slug}" references "${file}" which does not exist.`
99
+ });
100
+ }
101
+ }
102
+ return diagnostics;
103
+ }
104
+
105
+ export { checkManifest };
@@ -0,0 +1,195 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
+ import { Rule } from '../types.js';
4
+ import { getCodeBlockLines } from './utils.js';
5
+
6
+ const HTML_TAG_RE = /< *\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*\/?>/g;
7
+ const ALLOWED_HTML_TAGS = /* @__PURE__ */ new Set(["br", "wbr", "hr", "details", "summary"]);
8
+ const SCRIPT_TAG_RE = /<script\b[^>]*>/gi;
9
+ const EVENT_HANDLER_RE = /\bon[a-z]+\s*=\s*(?:["'][^"']*["']|[^\s>]+)/gi;
10
+ const IMAGE_REF_RE = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
11
+ const LINK_RE = /\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
12
+ const DANGEROUS_URL_RE = /^(javascript|vbscript|data):/i;
13
+ const BASE64_IMAGE_RE = /^data:image\/[^;]+;base64,/i;
14
+ const EXTERNAL_URL_RE = /^https?:\/\//i;
15
+ const PATH_TRAVERSAL_RE = /(?:^|\/)\.\.\//;
16
+ function matchOutsideCode(content, re, codeLines) {
17
+ const results = [];
18
+ const lines = content.split("\n");
19
+ for (let i = 0; i < lines.length; i++) {
20
+ if (codeLines.has(i + 1)) {
21
+ continue;
22
+ }
23
+ const lineRe = new RegExp(re.source, re.flags);
24
+ let m;
25
+ while ((m = lineRe.exec(lines[i])) !== null) {
26
+ results.push({ match: m, line: i + 1 });
27
+ }
28
+ }
29
+ return results;
30
+ }
31
+ async function checkMarkdown(input) {
32
+ const diagnostics = [];
33
+ let entries = [];
34
+ try {
35
+ entries = await readdir(input.docsPath, { recursive: true, withFileTypes: true });
36
+ } catch {
37
+ return diagnostics;
38
+ }
39
+ const mdFiles = entries.filter(
40
+ (e) => e.isFile() && e.name.endsWith(".md") && !e.parentPath.includes("node_modules") && !e.parentPath.includes("dist")
41
+ );
42
+ for (const md of mdFiles) {
43
+ const absPath = join(md.parentPath, md.name);
44
+ const relPath = relative(input.docsPath, absPath);
45
+ let content;
46
+ try {
47
+ content = await readFile(absPath, "utf-8");
48
+ } catch {
49
+ continue;
50
+ }
51
+ const codeLines = getCodeBlockLines(content);
52
+ for (const { match, line } of matchOutsideCode(content, SCRIPT_TAG_RE, codeLines)) {
53
+ diagnostics.push({
54
+ rule: Rule.NoScriptTags,
55
+ severity: "error",
56
+ file: relPath,
57
+ line,
58
+ title: "Script tag detected",
59
+ detail: `"${match[0]}" is not allowed. Script tags pose a security risk.`
60
+ });
61
+ }
62
+ for (const { match, line } of matchOutsideCode(content, EVENT_HANDLER_RE, codeLines)) {
63
+ diagnostics.push({
64
+ rule: Rule.NoScriptTags,
65
+ severity: "error",
66
+ file: relPath,
67
+ line,
68
+ title: "Event handler attribute detected",
69
+ detail: `"${match[0]}" is not allowed. Inline event handlers pose a security risk.`
70
+ });
71
+ }
72
+ for (const { match, line } of matchOutsideCode(content, HTML_TAG_RE, codeLines)) {
73
+ const tagName = match[1].toLowerCase();
74
+ if (tagName === "script" || ALLOWED_HTML_TAGS.has(tagName)) {
75
+ continue;
76
+ }
77
+ diagnostics.push({
78
+ rule: Rule.NoRawHtml,
79
+ severity: input.strict ? "error" : "warning",
80
+ file: relPath,
81
+ line,
82
+ title: "Raw HTML tag detected",
83
+ detail: `<${tagName}> is not allowed. Use markdown syntax instead of raw HTML.`
84
+ });
85
+ }
86
+ for (const { match, line } of matchOutsideCode(content, IMAGE_REF_RE, codeLines)) {
87
+ const ref = match[2];
88
+ if (BASE64_IMAGE_RE.test(ref)) {
89
+ diagnostics.push({
90
+ rule: Rule.NoBase64Images,
91
+ severity: "error",
92
+ file: relPath,
93
+ line,
94
+ title: "Base64-encoded image detected",
95
+ detail: `Base64-encoded images are not allowed. Save the image as a file in the img/ directory instead.`
96
+ });
97
+ continue;
98
+ }
99
+ if (EXTERNAL_URL_RE.test(ref)) {
100
+ diagnostics.push({
101
+ rule: Rule.NoExternalImages,
102
+ severity: input.strict ? "error" : "warning",
103
+ file: relPath,
104
+ line,
105
+ title: "External image URL detected",
106
+ detail: `"${ref}" is an external URL. Download the image and place it in the img/ directory.`
107
+ });
108
+ continue;
109
+ }
110
+ if (DANGEROUS_URL_RE.test(ref)) {
111
+ diagnostics.push({
112
+ rule: Rule.NoDangerousUrls,
113
+ severity: "error",
114
+ file: relPath,
115
+ line,
116
+ title: "Dangerous URI scheme in image reference",
117
+ detail: `"${ref}" uses a dangerous URI scheme. Only relative file paths are allowed.`
118
+ });
119
+ continue;
120
+ }
121
+ if (PATH_TRAVERSAL_RE.test(ref)) {
122
+ diagnostics.push({
123
+ rule: Rule.NoPathTraversal,
124
+ severity: "error",
125
+ file: relPath,
126
+ line,
127
+ title: "Path traversal in image reference",
128
+ detail: `"${ref}" contains path traversal. Image references must not use "../".`
129
+ });
130
+ continue;
131
+ }
132
+ if (ref.startsWith("/")) {
133
+ diagnostics.push({
134
+ rule: Rule.ImageRefsRelative,
135
+ severity: "error",
136
+ file: relPath,
137
+ line,
138
+ title: "Image reference is not a relative path",
139
+ detail: `"${ref}" is an absolute path. Use a relative path like "img/filename.png" instead.`
140
+ });
141
+ }
142
+ }
143
+ const contentLines = content.split("\n");
144
+ for (const { match, line } of matchOutsideCode(content, LINK_RE, codeLines)) {
145
+ const ref = match[2];
146
+ if (match.index > 0 && contentLines[line - 1][match.index - 1] === "!") {
147
+ continue;
148
+ }
149
+ if (ref.startsWith("#")) {
150
+ continue;
151
+ }
152
+ if (DANGEROUS_URL_RE.test(ref)) {
153
+ diagnostics.push({
154
+ rule: Rule.NoDangerousUrls,
155
+ severity: "error",
156
+ file: relPath,
157
+ line,
158
+ title: "Dangerous URI scheme in link",
159
+ detail: `"${ref}" uses a dangerous URI scheme. Use safe URLs only.`
160
+ });
161
+ continue;
162
+ }
163
+ if (PATH_TRAVERSAL_RE.test(ref)) {
164
+ diagnostics.push({
165
+ rule: Rule.NoPathTraversal,
166
+ severity: "error",
167
+ file: relPath,
168
+ line,
169
+ title: "Path traversal in link",
170
+ detail: `"${ref}" contains path traversal. Links must not use "../".`
171
+ });
172
+ continue;
173
+ }
174
+ if (EXTERNAL_URL_RE.test(ref)) {
175
+ continue;
176
+ }
177
+ if (/^[a-z]+:/i.test(ref)) {
178
+ continue;
179
+ }
180
+ if (ref.startsWith("/")) {
181
+ diagnostics.push({
182
+ rule: Rule.InternalLinksRelative,
183
+ severity: input.strict ? "error" : "warning",
184
+ file: relPath,
185
+ line,
186
+ title: "Internal link is not a relative path",
187
+ detail: `"${ref}" is an absolute path. Use a relative path like "./page.md" instead.`
188
+ });
189
+ }
190
+ }
191
+ }
192
+ return diagnostics;
193
+ }
194
+
195
+ export { checkMarkdown };
@@ -0,0 +1,18 @@
1
+ function getCodeBlockLines(content) {
2
+ const lines = content.split("\n");
3
+ const codeLines = /* @__PURE__ */ new Set();
4
+ let inCodeBlock = false;
5
+ for (let i = 0; i < lines.length; i++) {
6
+ if (/^```/.test(lines[i].trim())) {
7
+ inCodeBlock = !inCodeBlock;
8
+ codeLines.add(i + 1);
9
+ continue;
10
+ }
11
+ if (inCodeBlock) {
12
+ codeLines.add(i + 1);
13
+ }
14
+ }
15
+ return codeLines;
16
+ }
17
+
18
+ export { getCodeBlockLines };
@@ -0,0 +1,45 @@
1
+ const Rule = {
2
+ // filesystem rules
3
+ HasMarkdown: "has-markdown-files",
4
+ RootIndex: "root-index-exists",
5
+ NestedDirIndex: "nested-dir-has-index",
6
+ NoSpaces: "no-spaces-in-names",
7
+ ValidNaming: "valid-file-naming",
8
+ NoEmptyDir: "no-empty-directories",
9
+ NoSymlinks: "no-symlinks",
10
+ AllowedFileTypes: "allowed-file-types",
11
+ // frontmatter rules
12
+ BlockExists: "frontmatter-block-exists",
13
+ ValidYaml: "frontmatter-valid-yaml",
14
+ RequiredFields: "frontmatter-required-fields",
15
+ FieldTypes: "frontmatter-field-types",
16
+ ValidSlug: "frontmatter-valid-slug",
17
+ NoH1: "no-h1-heading",
18
+ DuplicatePosition: "no-duplicate-sidebar-position",
19
+ DuplicateSlug: "no-duplicate-slugs",
20
+ // asset rules
21
+ NoSvg: "no-svg-files",
22
+ ReferencedImagesExist: "referenced-images-exist",
23
+ MaxImageSize: "max-image-size",
24
+ MaxTotalImagesSize: "max-total-images-size",
25
+ ImageFileNaming: "image-file-naming",
26
+ NoOrphanedImages: "no-orphaned-images",
27
+ MaxDataUriSize: "max-data-uri-size",
28
+ // markdown + security rules
29
+ NoRawHtml: "no-raw-html",
30
+ ImageRefsRelative: "image-refs-relative",
31
+ InternalLinksRelative: "internal-links-relative",
32
+ NoDangerousUrls: "no-dangerous-urls",
33
+ NoScriptTags: "no-script-tags",
34
+ NoPathTraversal: "no-path-traversal",
35
+ NoBase64Images: "no-base64-images",
36
+ NoExternalImages: "no-external-images",
37
+ // cross-file rules
38
+ InternalLinksResolve: "internal-links-resolve",
39
+ AnchorLinksResolve: "anchor-links-resolve",
40
+ // manifest rules
41
+ ManifestValid: "manifest-valid",
42
+ ManifestRefsExist: "manifest-refs-exist"
43
+ };
44
+
45
+ export { Rule };
package/package.json CHANGED
@@ -1 +1,68 @@
1
- {"name":"@grafana/plugin-docs-cli","version":"0.0.0","publishConfig":{"access":"public"}}
1
+ {
2
+ "name": "@grafana/plugin-docs-cli",
3
+ "version": "0.0.10-canary.2537.23286312909.0",
4
+ "description": "CLI tool for developing, validating and previewing Grafana plugin documentation locally.",
5
+ "type": "module",
6
+ "bin": "./dist/bin/run.js",
7
+ "files": [
8
+ "dist",
9
+ "LICENSE",
10
+ "README.md",
11
+ "CHANGELOG.md"
12
+ ],
13
+ "repository": {
14
+ "directory": "packages/plugin-docs-cli",
15
+ "url": "https://github.com/grafana/plugin-tools"
16
+ },
17
+ "author": "Grafana",
18
+ "license": "Apache-2.0",
19
+ "publishConfig": {
20
+ "registry": "https://registry.npmjs.org/",
21
+ "access": "public",
22
+ "provenance": true
23
+ },
24
+ "scripts": {
25
+ "build": "rollup -c ../../rollup.config.ts --configPlugin esbuild",
26
+ "dev": "rollup -c ../../rollup.config.ts --configPlugin esbuild --watch",
27
+ "lint": "eslint --cache ./src",
28
+ "lint:fix": "npm run lint -- --fix",
29
+ "lint:package": "publint",
30
+ "test": "vitest",
31
+ "typecheck": "tsc --noEmit"
32
+ },
33
+ "keywords": [
34
+ "grafana",
35
+ "plugin",
36
+ "documentation",
37
+ "cli",
38
+ "preview",
39
+ "validation"
40
+ ],
41
+ "bugs": {
42
+ "url": "https://github.com/grafana/plugin-tools/issues"
43
+ },
44
+ "engines": {
45
+ "node": ">=24"
46
+ },
47
+ "dependencies": {
48
+ "@grafana/plugin-docs-parser": "0.0.4-canary.2537.23286312909.0",
49
+ "chokidar": "^5.0.0",
50
+ "debug": "^4.3.7",
51
+ "ejs": "^5.0.0",
52
+ "express": "^5.0.0",
53
+ "github-slugger": "^1.5.0",
54
+ "gray-matter": "^4.0.3",
55
+ "hast-util-to-html": "^9.0.0",
56
+ "minimist": "^1.2.8"
57
+ },
58
+ "devDependencies": {
59
+ "@types/debug": "^4.1.12",
60
+ "@types/ejs": "^3.1.5",
61
+ "@types/express": "^5.0.0",
62
+ "@types/github-slugger": "^1.3.0",
63
+ "@types/minimist": "^1.2.5",
64
+ "@types/supertest": "^7.0.0",
65
+ "supertest": "^7.0.0"
66
+ },
67
+ "gitHead": "557c04d508319ddcb39f34b34c1172ad5f483a07"
68
+ }