@grafana/create-plugin 7.13.0 → 7.15.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.
- package/CHANGELOG.md +24 -0
- package/dist/codemods/additions/additions.js +12 -0
- package/dist/codemods/additions/prompts/docs-content.md +131 -0
- package/dist/codemods/additions/scripts/docs-content.js +30 -0
- package/dist/codemods/additions/scripts/docs.js +468 -0
- package/dist/codemods/additions/scripts/experimental-app-sdk.js +15 -14
- package/dist/codemods/utils.agents.js +64 -0
- package/dist/codemods/utils.js +40 -7
- package/dist/codemods/utils.workflows.js +94 -0
- package/dist/utils/utils.cli.js +3 -2
- package/dist/utils/utils.handlebars.js +9 -2
- package/package.json +1 -1
- package/src/codemods/additions/additions.ts +12 -0
- package/src/codemods/additions/prompts/docs-content.md +131 -0
- package/src/codemods/additions/scripts/docs-content.test.ts +103 -0
- package/src/codemods/additions/scripts/docs-content.ts +32 -0
- package/src/codemods/additions/scripts/docs.test.ts +1766 -0
- package/src/codemods/additions/scripts/docs.ts +697 -0
- package/src/codemods/additions/scripts/experimental-app-sdk.test.ts +4 -3
- package/src/codemods/additions/scripts/experimental-app-sdk.ts +15 -15
- package/src/codemods/migrations/scripts/004-eslint9-flat-config.ts +1 -3
- package/src/codemods/utils.agents.test.ts +140 -0
- package/src/codemods/utils.agents.ts +81 -0
- package/src/codemods/utils.test.ts +23 -0
- package/src/codemods/utils.ts +29 -7
- package/src/codemods/utils.workflows.test.ts +153 -0
- package/src/codemods/utils.workflows.ts +140 -0
- package/src/utils/utils.cli.ts +2 -1
- package/src/utils/utils.handlebars.ts +9 -0
- package/templates/app-sdk/.config/app-sdk/generate-kinds.mjs +1 -1
- package/templates/docs/app/docs/AGENTS.md +127 -0
- package/templates/docs/app/docs/CLAUDE.md +1 -0
- package/templates/docs/app/docs/README.md +20 -0
- package/templates/docs/app/docs/configure.md +65 -0
- package/templates/docs/app/docs/dashboards.md +29 -0
- package/templates/docs/app/docs/index.md +70 -0
- package/templates/docs/app/docs/permissions.md +45 -0
- package/templates/docs/app/docs/troubleshooting.md +65 -0
- package/templates/docs/app/feature-page.md +26 -0
- package/templates/docs/app/features-index.md +12 -0
- package/templates/docs/common/workflows/validate-docs.yml +52 -0
- package/templates/docs/datasource/docs/AGENTS.md +123 -0
- package/templates/docs/datasource/docs/CLAUDE.md +1 -0
- package/templates/docs/datasource/docs/README.md +20 -0
- package/templates/docs/datasource/docs/alerting.md +56 -0
- package/templates/docs/datasource/docs/annotations.md +68 -0
- package/templates/docs/datasource/docs/configure.md +114 -0
- package/templates/docs/datasource/docs/dashboards.md +28 -0
- package/templates/docs/datasource/docs/index.md +40 -0
- package/templates/docs/datasource/docs/query-editor.md +72 -0
- package/templates/docs/datasource/docs/template-variables.md +69 -0
- package/templates/docs/datasource/docs/troubleshooting.md +73 -0
- package/templates/docs/panel/docs/AGENTS.md +113 -0
- package/templates/docs/panel/docs/CLAUDE.md +1 -0
- package/templates/docs/panel/docs/README.md +20 -0
- package/templates/docs/panel/docs/data-formats.md +27 -0
- package/templates/docs/panel/docs/examples.md +35 -0
- package/templates/docs/panel/docs/index.md +26 -0
- package/templates/docs/panel/docs/options.md +53 -0
- package/templates/docs/panel/docs/troubleshooting.md +35 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { join, basename } from 'node:path';
|
|
2
|
+
import * as v from 'valibot';
|
|
3
|
+
import { TEMPLATES_DIR, TEMPLATE_PATHS } from '../../../constants.js';
|
|
4
|
+
import { output } from '../../../utils/utils.console.js';
|
|
5
|
+
import { readJsonFile, getTemplateDataFromContext, addDependenciesToPackageJson, addTemplateFolder, renderTemplate, additionsDebug } from '../../utils.js';
|
|
6
|
+
import { ensureRootAgentFiles, ensureConfigAgentFiles, appendToAgentInstructions, AGENT_INSTRUCTIONS_PATH } from '../../utils.agents.js';
|
|
7
|
+
import { bumpActionRefs } from '../../utils.workflows.js';
|
|
8
|
+
import { getPackageManagerExecCmd } from '../../../utils/utils.packageManager.js';
|
|
9
|
+
import * as recast from 'recast';
|
|
10
|
+
import * as babelTsParser from 'recast/parsers/babel-ts.js';
|
|
11
|
+
|
|
12
|
+
const DOCS_PATH_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9_-]|\/[a-zA-Z0-9_-])*$/;
|
|
13
|
+
const DOCS_PATH_RULE = 'a relative path of letters, digits, hyphens, underscores and single "/" separators (for example "catalog-docs" or "docs/plugin")';
|
|
14
|
+
const DEFAULT_DOCS_PATH = "catalog-docs";
|
|
15
|
+
function isValidDocsPath(value) {
|
|
16
|
+
return typeof value === "string" && DOCS_PATH_RE.test(value);
|
|
17
|
+
}
|
|
18
|
+
function assertValidDocsPath(value, command, nextStep) {
|
|
19
|
+
if (!isValidDocsPath(value)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`src/plugin.json has docsPath set to '${value}', which create-plugin ${command} can't use.
|
|
22
|
+
Change it to ${DOCS_PATH_RULE}, then ${nextStep}.`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const schema = v.object({
|
|
27
|
+
docsPath: v.optional(
|
|
28
|
+
v.pipe(
|
|
29
|
+
v.string(),
|
|
30
|
+
v.minLength(1, "docsPath must not be empty."),
|
|
31
|
+
v.regex(
|
|
32
|
+
DOCS_PATH_RE,
|
|
33
|
+
`docsPath must be ${DOCS_PATH_RULE}, with no leading "/", "./", trailing "/", ".." segment, backslash or quote character.`
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
});
|
|
38
|
+
const PLUGIN_DOCS_CLI_VERSION = "^0.8.0";
|
|
39
|
+
const SUPPORTED_PLUGIN_TYPES = ["panel", "datasource", "app"];
|
|
40
|
+
function docs(context, options) {
|
|
41
|
+
const pluginType = assertSupportedPluginType(context);
|
|
42
|
+
return setupDocsScaffolding({
|
|
43
|
+
context,
|
|
44
|
+
docsPath: options.docsPath,
|
|
45
|
+
// `templates/docs` is left out of TEMPLATE_PATHS so `generate` ignores it
|
|
46
|
+
templateDir: join(TEMPLATES_DIR, "docs", pluginType),
|
|
47
|
+
commonTemplateDir: join(TEMPLATES_DIR, "docs", "common"),
|
|
48
|
+
featurePages: FEATURE_PAGES_BY_TYPE[pluginType],
|
|
49
|
+
pageStubs: pluginType === "app" ? getAppFeaturePageStubs : void 0
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function setupDocsScaffolding(opts) {
|
|
53
|
+
const { context, templateDir, commonTemplateDir } = opts;
|
|
54
|
+
const pluginJson = readJsonFile(context, "src/plugin.json");
|
|
55
|
+
const existingDocsPath = pluginJson.docsPath;
|
|
56
|
+
if (existingDocsPath !== void 0) {
|
|
57
|
+
assertValidDocsPath(existingDocsPath, "add docs", "run this again");
|
|
58
|
+
}
|
|
59
|
+
if (opts.docsPath !== void 0 && existingDocsPath !== void 0 && existingDocsPath !== opts.docsPath) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`src/plugin.json already has docsPath set to '${existingDocsPath}'.
|
|
62
|
+
Re-run with the existing path:
|
|
63
|
+
create-plugin add docs --docsPath ${existingDocsPath}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const docsPath = opts.docsPath ?? existingDocsPath ?? DEFAULT_DOCS_PATH;
|
|
67
|
+
if (existingDocsPath === void 0 && context.doesFileExistOnDisk(docsPath)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`A directory already exists at '${docsPath}' but src/plugin.json has no docsPath.
|
|
70
|
+
Point the plugin at it by setting "docsPath": "${docsPath}" in src/plugin.json, or scaffold elsewhere:
|
|
71
|
+
create-plugin add docs --docsPath <alternative-path>`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (!pluginJson.name) {
|
|
75
|
+
throw new Error('src/plugin.json has no "name". Add one - it is used as the title throughout the docs.');
|
|
76
|
+
}
|
|
77
|
+
if (!pluginJson.id) {
|
|
78
|
+
throw new Error('src/plugin.json has no "id". Add one - it is used in the catalog links throughout the docs.');
|
|
79
|
+
}
|
|
80
|
+
const pluginId = pluginJson.id;
|
|
81
|
+
if (existingDocsPath !== docsPath) {
|
|
82
|
+
context.updateFile("src/plugin.json", JSON.stringify({ ...pluginJson, docsPath }, null, 2));
|
|
83
|
+
}
|
|
84
|
+
const templateData = getTemplateDataFromContext(context);
|
|
85
|
+
const featurePages = (opts.featurePages ?? []).map(({ page, templateFlag, hasFeature }) => ({
|
|
86
|
+
page,
|
|
87
|
+
templateFlag,
|
|
88
|
+
isEnabled: hasFeature(pluginJson, context)
|
|
89
|
+
}));
|
|
90
|
+
const featureFlags = Object.fromEntries(featurePages.map(({ templateFlag, isEnabled }) => [templateFlag, isEnabled]));
|
|
91
|
+
const pageStubs = opts.pageStubs?.(pluginJson, featureFlags) ?? { data: {}, stubs: [], skipped: [] };
|
|
92
|
+
const { packageManagerName, packageManagerVersion } = templateData;
|
|
93
|
+
const createPluginCmd = `${getPackageManagerExecCmd(packageManagerName, packageManagerVersion)}@latest`;
|
|
94
|
+
const data = {
|
|
95
|
+
...templateData,
|
|
96
|
+
pluginId,
|
|
97
|
+
docsPath,
|
|
98
|
+
createPluginCmd,
|
|
99
|
+
lockFile: LOCK_FILES[packageManagerName],
|
|
100
|
+
...featureFlags,
|
|
101
|
+
...pageStubs.data
|
|
102
|
+
};
|
|
103
|
+
addDependenciesToPackageJson(context, {}, { "@grafana/plugin-docs-cli": PLUGIN_DOCS_CLI_VERSION });
|
|
104
|
+
addDocsScripts(context);
|
|
105
|
+
const hasPages = hasDocsPages(context, docsPath);
|
|
106
|
+
const disabledPageFiles = featurePages.filter(({ isEnabled }) => !isEnabled).map(({ page }) => `${page}.md`);
|
|
107
|
+
addTemplateFolder(context, {
|
|
108
|
+
folderPath: join(templateDir, "docs"),
|
|
109
|
+
targetPath: docsPath,
|
|
110
|
+
data,
|
|
111
|
+
include: (file) => SCAFFOLD_META_FILES.includes(file) || !hasPages && !disabledPageFiles.includes(file)
|
|
112
|
+
});
|
|
113
|
+
const report = {
|
|
114
|
+
missingPageStubs: scaffoldPageStubs({ context, docsPath, stubs: pageStubs.stubs, data, hasPages }),
|
|
115
|
+
// a feature turned on after the first scaffold gets no page, so point the author at it instead
|
|
116
|
+
missingFeaturePages: hasPages ? featurePages.filter(({ page, isEnabled }) => isEnabled && !pageExists(context, docsPath, [page])).map(({ page }) => `${page}.md`) : [],
|
|
117
|
+
// a re-run has no pages to compare against, so it only reports what the first scaffold left out
|
|
118
|
+
skippedPages: hasPages ? [] : pageStubs.skipped
|
|
119
|
+
};
|
|
120
|
+
const workflowPath = ".github/workflows/validate-docs.yml";
|
|
121
|
+
if (!context.doesFileExist(workflowPath)) {
|
|
122
|
+
context.addFile(workflowPath, renderTemplate(join(commonTemplateDir, "workflows/validate-docs.yml"), false, data));
|
|
123
|
+
} else {
|
|
124
|
+
additionsDebug(`${workflowPath} already exists, skipping`);
|
|
125
|
+
}
|
|
126
|
+
const pluginActions = bumpActionRefs(context, RELEASE_WORKFLOW_PATH, REQUIRED_ACTION_REFS);
|
|
127
|
+
const linkedRootFiles = ensureRootAgentFiles(context, TEMPLATE_PATHS.common);
|
|
128
|
+
ensureConfigAgentFiles(context, TEMPLATE_PATHS.common, CONFIG_AGENT_FILES, data);
|
|
129
|
+
appendDocsPointerToInstructions(context, docsPath);
|
|
130
|
+
if (context.hasChanges() || report.missingFeaturePages.length > 0 || report.missingPageStubs.length > 0) {
|
|
131
|
+
context.setMessage(
|
|
132
|
+
buildNextStepsMessage({ docsPath, packageManagerName, createPluginCmd, pluginActions, linkedRootFiles, report })
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return context;
|
|
136
|
+
}
|
|
137
|
+
function pageExists(context, docsPath, pageNames) {
|
|
138
|
+
return pageNames.some(
|
|
139
|
+
(name) => context.doesFileExist(`${docsPath}/${name}.md`) || context.doesFileExist(`${docsPath}/${name}`)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
function scaffoldPageStubs(opts) {
|
|
143
|
+
const { context, docsPath, stubs, data, hasPages } = opts;
|
|
144
|
+
const missingPages = [];
|
|
145
|
+
const missingIndexes = [];
|
|
146
|
+
for (const stub of stubs) {
|
|
147
|
+
const filePath = `${docsPath}/${stub.file}`;
|
|
148
|
+
if (hasPages) {
|
|
149
|
+
if (stub.slug !== void 0) {
|
|
150
|
+
if (!pageExists(context, docsPath, [stub.slug, `features/${stub.slug}`])) {
|
|
151
|
+
missingPages.push(filePath);
|
|
152
|
+
}
|
|
153
|
+
} else if (!context.doesFileExist(filePath)) {
|
|
154
|
+
missingIndexes.push(filePath);
|
|
155
|
+
}
|
|
156
|
+
} else if (context.doesFileExist(filePath)) {
|
|
157
|
+
additionsDebug(`${filePath} already exists, skipping`);
|
|
158
|
+
} else {
|
|
159
|
+
context.addFile(filePath, renderTemplate(stub.templatePath, false, { ...data, ...stub.data }));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const missing = [...missingPages, ...missingIndexes];
|
|
163
|
+
return missing;
|
|
164
|
+
}
|
|
165
|
+
function buildNextStepsMessage(opts) {
|
|
166
|
+
const { docsPath, packageManagerName, createPluginCmd, pluginActions, linkedRootFiles } = opts;
|
|
167
|
+
const { missingFeaturePages, missingPageStubs, skippedPages } = opts.report;
|
|
168
|
+
const run = (script) => output.formatCode(`${packageManagerName} run ${script}`);
|
|
169
|
+
const steps = [
|
|
170
|
+
{
|
|
171
|
+
title: `Read ${docsPath}/README.md`,
|
|
172
|
+
detail: "It has the commands and a link to the plugin docs guide."
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
title: "Write the pages, yourself or with an AI agent",
|
|
176
|
+
detail: [
|
|
177
|
+
`Fill in or review the pages under ${docsPath}/ by hand.`,
|
|
178
|
+
"Or commit these changes, then have an agent draft them from your source code and README:",
|
|
179
|
+
output.formatCode(`${createPluginCmd} add docs-content`),
|
|
180
|
+
"Review its work either way."
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
title: "Preview the pages",
|
|
185
|
+
detail: `Run ${run("docs:serve")}, which also lists every section left to fill.`
|
|
186
|
+
}
|
|
187
|
+
];
|
|
188
|
+
const warnings = [];
|
|
189
|
+
if (pluginActions.found.length === 0) {
|
|
190
|
+
warnings.push({
|
|
191
|
+
title: `No build-plugin or package-plugin step found in ${RELEASE_WORKFLOW_PATH}`,
|
|
192
|
+
detail: `Make sure your release pipeline builds ${docsPath}/ into the plugin archive, or the docs won't publish.`
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
for (const action of pluginActions.unrecognized) {
|
|
196
|
+
warnings.push({
|
|
197
|
+
title: `${RELEASE_WORKFLOW_PATH} pins ${action} to a branch or commit, so it was left as is`,
|
|
198
|
+
detail: `Docs need ${REQUIRED_ACTION_REFS[action]} or later.`
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
for (const page of missingFeaturePages) {
|
|
202
|
+
warnings.push({
|
|
203
|
+
title: `src/plugin.json declares the feature ${docsPath}/${page} documents, but the page doesn't exist`,
|
|
204
|
+
detail: `If it needs docs, add the page as described under Optional pages in ${docsPath}/AGENTS.md. If you deleted it on purpose, ignore this.`
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (missingPageStubs.length > 0) {
|
|
208
|
+
warnings.push({
|
|
209
|
+
title: `src/plugin.json has pages with no docs page, such as ${missingPageStubs[0]}`,
|
|
210
|
+
detail: `If they need docs, add them as described under Optional pages in ${docsPath}/AGENTS.md. If you deleted them on purpose, ignore this.`
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (skippedPages.length > 0) {
|
|
214
|
+
warnings.push({
|
|
215
|
+
title: `Left out of the docs: ${skippedPages.join(", ")}`,
|
|
216
|
+
detail: `If they need docs, add them as described under Optional pages in ${docsPath}/AGENTS.md.`
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
for (const file of linkedRootFiles) {
|
|
220
|
+
warnings.push({
|
|
221
|
+
title: `Added a pointer to ${AGENT_INSTRUCTIONS_PATH} at the end of ${file}`,
|
|
222
|
+
detail: "Agents that read it now also load the plugin instructions. Check it fits with the rest of the file."
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const body = steps.flatMap((step, i) => formatNextStep(step, ` ${i + 1}. `));
|
|
226
|
+
if (warnings.length > 0) {
|
|
227
|
+
body.push("", "Needs your attention:", ...warnings.flatMap((warning) => formatNextStep(warning, " ! ")));
|
|
228
|
+
}
|
|
229
|
+
return { level: "log", title: "Next steps", body };
|
|
230
|
+
}
|
|
231
|
+
function formatNextStep({ title, detail }, marker) {
|
|
232
|
+
const indent = " ".repeat(marker.length);
|
|
233
|
+
return [`${marker}${title}`, ...[detail ?? []].flat().map((line) => `${indent}${line}`)];
|
|
234
|
+
}
|
|
235
|
+
function assertSupportedPluginType(context, additionName = "docs") {
|
|
236
|
+
const { type } = readJsonFile(context, "src/plugin.json");
|
|
237
|
+
if (isSupportedPluginType(type)) {
|
|
238
|
+
return type;
|
|
239
|
+
}
|
|
240
|
+
const supported = SUPPORTED_PLUGIN_TYPES.join(", ");
|
|
241
|
+
throw new Error(
|
|
242
|
+
`create-plugin add ${additionName} needs a plugin type of ${supported} in src/plugin.json, but found '${type ?? "unset"}'.`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
function isSupportedPluginType(type) {
|
|
246
|
+
return SUPPORTED_PLUGIN_TYPES.includes(type);
|
|
247
|
+
}
|
|
248
|
+
const CONFIG_AGENT_FILES = ["instructions.md", "e2e-testing.md"];
|
|
249
|
+
const SCAFFOLD_META_FILES = ["README.md", "AGENTS.md", "CLAUDE.md"];
|
|
250
|
+
const DASHBOARDS_PAGE = {
|
|
251
|
+
page: "dashboards",
|
|
252
|
+
templateFlag: "hasDashboards",
|
|
253
|
+
hasFeature: (p) => getIncludes(p).some((i) => i.type === "dashboard")
|
|
254
|
+
};
|
|
255
|
+
const FEATURE_PAGES_BY_TYPE = {
|
|
256
|
+
panel: [],
|
|
257
|
+
datasource: [
|
|
258
|
+
{ page: "annotations", templateFlag: "hasAnnotations", hasFeature: (p) => p.annotations === true },
|
|
259
|
+
{ page: "alerting", templateFlag: "hasAlerting", hasFeature: (p) => p.alerting === true },
|
|
260
|
+
DASHBOARDS_PAGE
|
|
261
|
+
],
|
|
262
|
+
app: [
|
|
263
|
+
{ page: "configure", templateFlag: "hasConfigPage", hasFeature: hasAppConfigPage },
|
|
264
|
+
DASHBOARDS_PAGE,
|
|
265
|
+
{
|
|
266
|
+
page: "permissions",
|
|
267
|
+
templateFlag: "hasPermissions",
|
|
268
|
+
hasFeature: (p) => p.roles !== void 0 || p.iam !== void 0 || getIncludes(p).some((i) => i.role !== void 0 && i.role !== "Viewer" || i.action !== void 0)
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
};
|
|
272
|
+
function getIncludes(pluginJson) {
|
|
273
|
+
return Array.isArray(pluginJson.includes) ? pluginJson.includes.filter((i) => typeof i === "object" && i !== null) : [];
|
|
274
|
+
}
|
|
275
|
+
function isAppPath(path, pluginId, prefix) {
|
|
276
|
+
return [pluginId, "%PLUGIN_ID%"].some(
|
|
277
|
+
(id) => id && (path === `${prefix}${id}` || path?.startsWith(`${prefix}${id}/`))
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const APP_MODULE_FILES = ["src/module.ts", "src/module.tsx", "src/module.js", "src/module.jsx"];
|
|
281
|
+
function hasAppConfigPage(pluginJson, context) {
|
|
282
|
+
return APP_MODULE_FILES.some((file) => callsAddConfigPage(context.getFile(file))) || getIncludes(pluginJson).some((i) => i.type === "page" && isAppPath(i.path, pluginJson.id, "/plugins/"));
|
|
283
|
+
}
|
|
284
|
+
function callsAddConfigPage(source) {
|
|
285
|
+
if (!source) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
let ast;
|
|
289
|
+
try {
|
|
290
|
+
ast = recast.parse(source, { parser: babelTsParser });
|
|
291
|
+
} catch (error) {
|
|
292
|
+
additionsDebug(`Failed to parse the app module, so it can't tell whether it has a config page: ${error}`);
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
let isFound = false;
|
|
296
|
+
recast.visit(ast, {
|
|
297
|
+
visitCallExpression(path) {
|
|
298
|
+
const { callee } = path.node;
|
|
299
|
+
if (callee.type === "MemberExpression" && callee.property.type === "Identifier" && callee.property.name === "addConfigPage") {
|
|
300
|
+
isFound = true;
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
this.traverse(path);
|
|
304
|
+
return void 0;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
return isFound;
|
|
308
|
+
}
|
|
309
|
+
const TEMPLATE_DEFAULT_PAGES = {
|
|
310
|
+
"Page One": "/one",
|
|
311
|
+
"Page Two": "/two",
|
|
312
|
+
"Page Three": "/three",
|
|
313
|
+
"Page Four": "/four"
|
|
314
|
+
};
|
|
315
|
+
const CONFIG_PAGE_SLUGS = ["configuration", "configure"];
|
|
316
|
+
function getAppFeaturePages(pluginJson, hasConfigPage) {
|
|
317
|
+
const pages = [];
|
|
318
|
+
let configPage;
|
|
319
|
+
const skipped = [];
|
|
320
|
+
const appPages = getIncludes(pluginJson).filter((i) => i.type === "page" && typeof i.name === "string" && isAppPath(i.path, pluginJson.id, "/a/")).sort((a, b) => Number(b.defaultNav === true) - Number(a.defaultNav === true));
|
|
321
|
+
const takenSlugs = new Set(getReservedPageSlugs());
|
|
322
|
+
for (const include of appPages) {
|
|
323
|
+
const name = include.name;
|
|
324
|
+
const path = include.path.replaceAll("%PLUGIN_ID%", pluginJson.id ?? "");
|
|
325
|
+
const defaultPath = TEMPLATE_DEFAULT_PAGES[name];
|
|
326
|
+
if (defaultPath !== void 0 && path.endsWith(defaultPath)) {
|
|
327
|
+
additionsDebug(`${name} is an example page from the create-plugin app template, skipping`);
|
|
328
|
+
skipped.push(`${name} (${include.path}, an example page from the create-plugin app template)`);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const slug = slugify(name);
|
|
332
|
+
if (!slug) {
|
|
333
|
+
additionsDebug(`${name} has no letters or digits for a file name, skipping`);
|
|
334
|
+
skipped.push(`${name} (${include.path}, its name has no letters or digits for a file name)`);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const role = typeof include.role === "string" && include.role !== "Viewer" ? include.role : void 0;
|
|
338
|
+
if (!hasConfigPage && !configPage && CONFIG_PAGE_SLUGS.includes(slug)) {
|
|
339
|
+
configPage = { name, slug: "configure", path, role };
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (takenSlugs.has(slug)) {
|
|
343
|
+
additionsDebug(`${name} would be written to ${slug}.md, which another page uses, skipping`);
|
|
344
|
+
skipped.push(`${name} (${include.path}, its file name is used by another page)`);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
takenSlugs.add(slug);
|
|
348
|
+
pages.push({ name, slug, path, role });
|
|
349
|
+
}
|
|
350
|
+
return { pages, configPage, skipped };
|
|
351
|
+
}
|
|
352
|
+
function slugify(name) {
|
|
353
|
+
return name.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
354
|
+
}
|
|
355
|
+
const MAX_FLAT_PAGES = 3;
|
|
356
|
+
const FIRST_PAGE_POSITION = 3;
|
|
357
|
+
const CONFIG_PAGE_POSITION = 2;
|
|
358
|
+
const FIXED_PAGE_SLUGS = ["index", "configure", "dashboards", "permissions", "troubleshooting"];
|
|
359
|
+
function getReservedPageSlugs() {
|
|
360
|
+
return [
|
|
361
|
+
...FIXED_PAGE_SLUGS,
|
|
362
|
+
...DOCS_CLI_META_FILES.map((file) => file.slice(0, -".MD".length).toLowerCase().replaceAll("_", "-"))
|
|
363
|
+
];
|
|
364
|
+
}
|
|
365
|
+
function getAppFeaturePageStubs(pluginJson, featureFlags) {
|
|
366
|
+
const { pages, configPage, skipped } = getAppFeaturePages(pluginJson, featureFlags.hasConfigPage === true);
|
|
367
|
+
const templateDir = join(TEMPLATES_DIR, "docs", "app");
|
|
368
|
+
const pageTemplate = join(templateDir, "feature-page.md");
|
|
369
|
+
const stubs = [];
|
|
370
|
+
if (configPage) {
|
|
371
|
+
stubs.push({
|
|
372
|
+
templatePath: pageTemplate,
|
|
373
|
+
file: "configure.md",
|
|
374
|
+
slug: "configure",
|
|
375
|
+
data: { page: { ...configPage, position: CONFIG_PAGE_POSITION } }
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
const isInFolder = pages.length > MAX_FLAT_PAGES;
|
|
379
|
+
const folder = isInFolder ? "features/" : "";
|
|
380
|
+
if (isInFolder) {
|
|
381
|
+
stubs.push({
|
|
382
|
+
templatePath: join(templateDir, "features-index.md"),
|
|
383
|
+
file: "features/index.md",
|
|
384
|
+
data: { position: FIRST_PAGE_POSITION }
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const firstPosition = isInFolder ? FIRST_PAGE_POSITION + 1 : FIRST_PAGE_POSITION;
|
|
388
|
+
pages.forEach((page, i) => {
|
|
389
|
+
stubs.push({
|
|
390
|
+
templatePath: pageTemplate,
|
|
391
|
+
file: `${folder}${page.slug}.md`,
|
|
392
|
+
slug: page.slug,
|
|
393
|
+
data: { page: { ...page, position: firstPosition + i } }
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
return {
|
|
397
|
+
data: {
|
|
398
|
+
...configPage ? { hasConfigPage: true } : {},
|
|
399
|
+
...pages.length > 0 ? { defaultFeaturePage: { ...pages[0], file: `${folder}${pages[0].slug}` } } : {}
|
|
400
|
+
},
|
|
401
|
+
stubs,
|
|
402
|
+
skipped
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
const DOCS_CLI_META_FILES = [
|
|
406
|
+
"README.MD",
|
|
407
|
+
"CONTRIBUTING.MD",
|
|
408
|
+
"LICENSE.MD",
|
|
409
|
+
"CODE_OF_CONDUCT.MD",
|
|
410
|
+
"SECURITY.MD",
|
|
411
|
+
"CHANGELOG.MD",
|
|
412
|
+
"AGENTS.MD",
|
|
413
|
+
"CLAUDE.MD"
|
|
414
|
+
];
|
|
415
|
+
function hasDocsPages(context, docsPath) {
|
|
416
|
+
const addedInContext = Object.entries(context.listChanges()).filter(([path, change]) => change.changeType === "add" && path.startsWith(`${docsPath}/`)).map(([path]) => path);
|
|
417
|
+
return [...listFiles(context, docsPath), ...addedInContext].some(
|
|
418
|
+
(file) => file.endsWith(".md") && !DOCS_CLI_META_FILES.includes(basename(file).toUpperCase()) && !isInSkippedFolder(file.slice(docsPath.length + 1))
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
const SKIPPED_FOLDERS = ["node_modules", "dist"];
|
|
422
|
+
function isInSkippedFolder(relativePath) {
|
|
423
|
+
return relativePath.split("/").some((segment) => SKIPPED_FOLDERS.includes(segment));
|
|
424
|
+
}
|
|
425
|
+
function listFiles(context, folder) {
|
|
426
|
+
return context.readDir(folder).flatMap((entry) => SKIPPED_FOLDERS.includes(basename(entry)) ? [] : [entry, ...listFiles(context, entry)]);
|
|
427
|
+
}
|
|
428
|
+
function appendDocsPointerToInstructions(context, docsPath) {
|
|
429
|
+
const agentsPath = `${docsPath}/AGENTS.md`;
|
|
430
|
+
const section = `## Plugin docs
|
|
431
|
+
|
|
432
|
+
Whenever you add, remove or change a feature in the plugin, you must update the plugin documentation under \`${docsPath}/\`. Refer to [\`${agentsPath}\`](../../${agentsPath}) for instructions.
|
|
433
|
+
`;
|
|
434
|
+
appendToAgentInstructions(context, agentsPath, section);
|
|
435
|
+
}
|
|
436
|
+
const LOCK_FILES = {
|
|
437
|
+
npm: "package-lock.json",
|
|
438
|
+
pnpm: "pnpm-lock.yaml",
|
|
439
|
+
yarn: "yarn.lock"
|
|
440
|
+
};
|
|
441
|
+
const RELEASE_WORKFLOW_PATH = ".github/workflows/release.yml";
|
|
442
|
+
const REQUIRED_ACTION_REFS = {
|
|
443
|
+
"grafana/plugin-actions/build-plugin": "build-plugin/v1.2.0",
|
|
444
|
+
"grafana/plugin-actions/package-plugin": "package-plugin/v1.3.3"
|
|
445
|
+
};
|
|
446
|
+
const DOCS_SCRIPTS = {
|
|
447
|
+
"docs:serve": "plugin-docs-cli serve --port 3001 --reload",
|
|
448
|
+
"docs:validate": "plugin-docs-cli validate"
|
|
449
|
+
};
|
|
450
|
+
function addDocsScripts(context) {
|
|
451
|
+
const packageJson = readJsonFile(context, "package.json");
|
|
452
|
+
const scripts = packageJson["scripts"] ?? {};
|
|
453
|
+
const missing = Object.entries(DOCS_SCRIPTS).filter(([name]) => {
|
|
454
|
+
if (scripts[name]) {
|
|
455
|
+
additionsDebug(`${name} already exists in package.json scripts, skipping`);
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
return true;
|
|
459
|
+
});
|
|
460
|
+
if (missing.length > 0) {
|
|
461
|
+
context.updateFile(
|
|
462
|
+
"package.json",
|
|
463
|
+
JSON.stringify({ ...packageJson, scripts: { ...scripts, ...Object.fromEntries(missing) } }, null, 2)
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export { assertSupportedPluginType, assertValidDocsPath, docs as default, hasDocsPages, isValidDocsPath, schema, setupDocsScaffolding };
|
|
@@ -4,9 +4,14 @@ import { parseDocument, YAMLMap, Scalar, stringify } from 'yaml';
|
|
|
4
4
|
import { getBackendCmd } from '../../../commands/generate/print-success-message.js';
|
|
5
5
|
import { output } from '../../../utils/utils.console.js';
|
|
6
6
|
import { getTemplateDataFromContext, additionsDebug, renderTemplate } from '../../utils.js';
|
|
7
|
+
import { AGENT_INSTRUCTIONS_PATH, appendToAgentInstructions } from '../../utils.agents.js';
|
|
7
8
|
import { addRequireToGoMod } from '../../utils.goMod.js';
|
|
8
9
|
|
|
9
|
-
const APP_SDK_FEATURE_TOGGLES = [
|
|
10
|
+
const APP_SDK_FEATURE_TOGGLES = [
|
|
11
|
+
"appplugins.loadAppManifest",
|
|
12
|
+
"appplugins.registerAPIServer",
|
|
13
|
+
"grafana.useRouterMiddleware"
|
|
14
|
+
];
|
|
10
15
|
const TEMPLATE_FILES = [
|
|
11
16
|
[".config/app-sdk/generate-kinds.mjs", true],
|
|
12
17
|
[".config/app-sdk/README.md", false],
|
|
@@ -78,23 +83,19 @@ function renderAppSdkTemplate(file, data, includeWarning = false) {
|
|
|
78
83
|
return renderTemplate(templatePath(file), includeWarning, data);
|
|
79
84
|
}
|
|
80
85
|
function referenceAgentInstructions(context, data) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!instructions) {
|
|
84
|
-
additionsDebug(`Could not find ${path}. Skipping the app-sdk reference.`);
|
|
86
|
+
if (!context.doesFileExist(AGENT_INSTRUCTIONS_PATH)) {
|
|
87
|
+
additionsDebug(`Could not find ${AGENT_INSTRUCTIONS_PATH}. Skipping the app-sdk reference.`);
|
|
85
88
|
return;
|
|
86
89
|
}
|
|
87
90
|
if (!context.doesFileExist(APP_SDK_MD)) {
|
|
88
91
|
context.addFile(APP_SDK_MD, renderAppSdkTemplate(APP_SDK_MD, data));
|
|
89
92
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
- This plugin defines its API resources as **CUE kinds** under \`kinds/\`, with TypeScript and Go types generated from them. Read @./${APP_SDK_MD} before changing anything under \`kinds/\` or any generated directory. **Never hand-edit generated code.**
|
|
97
|
-
`
|
|
93
|
+
appendToAgentInstructions(
|
|
94
|
+
context,
|
|
95
|
+
APP_SDK_MD,
|
|
96
|
+
`- This plugin defines its API resources as **CUE kinds** under \`kinds/\`, with TypeScript and Go types generated from them. Read @./${APP_SDK_MD} before changing anything under \`kinds/\` or any generated directory. **Never hand-edit generated code.**
|
|
97
|
+
`,
|
|
98
|
+
"\n"
|
|
98
99
|
);
|
|
99
100
|
}
|
|
100
101
|
function addGenerateScript(context) {
|
|
@@ -164,7 +165,7 @@ function wireGoBackend(context, data) {
|
|
|
164
165
|
wireMainGo(context);
|
|
165
166
|
addGoModDependency(context);
|
|
166
167
|
}
|
|
167
|
-
const GRAFANA_APP_SDK_VERSION = "v0.60.
|
|
168
|
+
const GRAFANA_APP_SDK_VERSION = "v0.60.16";
|
|
168
169
|
function addGoModDependency(context) {
|
|
169
170
|
addRequireToGoMod(context, "github.com/grafana/grafana-app-sdk", GRAFANA_APP_SDK_VERSION);
|
|
170
171
|
addRequireToGoMod(context, "github.com/grafana/grafana-app-sdk/plugin", GRAFANA_APP_SDK_VERSION);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { addTemplateFolder, codemodsDebug } from './utils.js';
|
|
4
|
+
|
|
5
|
+
const AGENT_INSTRUCTIONS_PATH = ".config/AGENTS/instructions.md";
|
|
6
|
+
const ROOT_AGENT_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"];
|
|
7
|
+
const INSTRUCTIONS_POINTER = `## Project knowledge
|
|
8
|
+
This repository contains a **Grafana plugin**. You must Read @./${AGENT_INSTRUCTIONS_PATH} before doing changes.
|
|
9
|
+
`;
|
|
10
|
+
const IMPORTS_INSTRUCTIONS = /@(\.\/)?\.config\/AGENTS\/instructions\.md/;
|
|
11
|
+
const IMPORTS_AGENTS_MD = /(^|\s)@(\.\/)?AGENTS\.md\b/m;
|
|
12
|
+
function ensureRootAgentFiles(context, templateDir) {
|
|
13
|
+
const existing = ROOT_AGENT_FILES.filter((file) => context.doesFileExist(file));
|
|
14
|
+
for (const file of ROOT_AGENT_FILES) {
|
|
15
|
+
if (!existing.includes(file)) {
|
|
16
|
+
context.addFile(file, readFileSync(join(templateDir, file), "utf-8"));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const reachesInstructions = (file) => {
|
|
20
|
+
const content = context.getFile(file) ?? "";
|
|
21
|
+
if (file === "AGENTS.md") {
|
|
22
|
+
return content.includes(AGENT_INSTRUCTIONS_PATH);
|
|
23
|
+
}
|
|
24
|
+
return IMPORTS_INSTRUCTIONS.test(content) || IMPORTS_AGENTS_MD.test(content) && reachesInstructions("AGENTS.md");
|
|
25
|
+
};
|
|
26
|
+
const linked = [];
|
|
27
|
+
for (const file of existing) {
|
|
28
|
+
if (reachesInstructions(file)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const content = context.getFile(file) ?? "";
|
|
32
|
+
context.updateFile(file, `${content.trimEnd()}
|
|
33
|
+
|
|
34
|
+
${INSTRUCTIONS_POINTER}`);
|
|
35
|
+
linked.push(file);
|
|
36
|
+
}
|
|
37
|
+
return linked;
|
|
38
|
+
}
|
|
39
|
+
function ensureConfigAgentFiles(context, templateDir, files, data) {
|
|
40
|
+
if (context.doesFileExist(AGENT_INSTRUCTIONS_PATH)) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return addTemplateFolder(context, {
|
|
44
|
+
folderPath: join(templateDir, ".config", "AGENTS"),
|
|
45
|
+
targetPath: ".config/AGENTS",
|
|
46
|
+
data,
|
|
47
|
+
include: (file) => files.includes(file)
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function appendToAgentInstructions(context, marker, text, separator = "\n\n") {
|
|
51
|
+
const existing = context.getFile(AGENT_INSTRUCTIONS_PATH);
|
|
52
|
+
if (existing === void 0) {
|
|
53
|
+
codemodsDebug(`Could not find ${AGENT_INSTRUCTIONS_PATH}, skipping`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (existing.includes(marker)) {
|
|
57
|
+
codemodsDebug(`${AGENT_INSTRUCTIONS_PATH} already contains ${marker}, skipping`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const kept = existing.trimEnd();
|
|
61
|
+
context.updateFile(AGENT_INSTRUCTIONS_PATH, kept ? `${kept}${separator}${text}` : text);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { AGENT_INSTRUCTIONS_PATH, INSTRUCTIONS_POINTER, appendToAgentInstructions, ensureConfigAgentFiles, ensureRootAgentFiles };
|
package/dist/codemods/utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { join, dirname } from 'node:path';
|
|
1
|
+
import { join, dirname, posix } from 'node:path';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
|
|
4
|
-
import 'glob';
|
|
3
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { glob } from 'glob';
|
|
5
5
|
import { styleText } from 'node:util';
|
|
6
6
|
import { output } from '../utils/utils.console.js';
|
|
7
7
|
import { getPackageManagerWithFallback, getPackageManagerSilentInstallCmd } from '../utils/utils.packageManager.js';
|
|
@@ -12,8 +12,7 @@ import { debug } from '../utils/utils.cli.js';
|
|
|
12
12
|
import { renderHandlebarsTemplate } from '../utils/utils.handlebars.js';
|
|
13
13
|
import { buildTemplateData, getTemplateData } from '../utils/utils.templates.js';
|
|
14
14
|
import { getConfig } from '../utils/utils.config.js';
|
|
15
|
-
import '../
|
|
16
|
-
import 'fs/promises';
|
|
15
|
+
import { getExportFileName } from '../utils/utils.files.js';
|
|
17
16
|
|
|
18
17
|
function printChanges(context, key, description) {
|
|
19
18
|
const changes = context.listChanges();
|
|
@@ -135,11 +134,16 @@ function readJsonFile(context, path) {
|
|
|
135
134
|
if (!context.doesFileExist(path)) {
|
|
136
135
|
throw new Error(`Cannot find ${path}`);
|
|
137
136
|
}
|
|
137
|
+
let parsed;
|
|
138
138
|
try {
|
|
139
|
-
|
|
139
|
+
parsed = JSON.parse(context.getFile(path) || "");
|
|
140
140
|
} catch (e) {
|
|
141
141
|
throw new Error(`Cannot parse ${path}: ${e}`);
|
|
142
142
|
}
|
|
143
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
144
|
+
throw new Error(`${path} does not contain a JSON object.`);
|
|
145
|
+
}
|
|
146
|
+
return parsed;
|
|
143
147
|
}
|
|
144
148
|
function addDependenciesToPackageJson(context, dependencies, devDependencies = {}, packageJsonPath = "package.json") {
|
|
145
149
|
const currentPackageJson = readJsonFile(context, packageJsonPath);
|
|
@@ -236,6 +240,7 @@ function sortObjectByKeys(obj) {
|
|
|
236
240
|
}
|
|
237
241
|
const migrationsDebug = debug.extend("migrations");
|
|
238
242
|
const additionsDebug = debug.extend("additions");
|
|
243
|
+
const codemodsDebug = debug.extend("codemods");
|
|
239
244
|
function renderTemplate(templatePath, includeWarning = false, data = getTemplateData()) {
|
|
240
245
|
let rendered = renderHandlebarsTemplate(readFileSync(templatePath, "utf-8"), data);
|
|
241
246
|
const regex = /DO NOT EDIT THIS FILE DIRECTLY\./g;
|
|
@@ -253,9 +258,37 @@ function getTemplateDataFromContext(context) {
|
|
|
253
258
|
features: getConfig(context.basePath).features
|
|
254
259
|
});
|
|
255
260
|
}
|
|
261
|
+
function addTemplateFolder(context, {
|
|
262
|
+
folderPath,
|
|
263
|
+
targetPath,
|
|
264
|
+
data,
|
|
265
|
+
include = () => true
|
|
266
|
+
}) {
|
|
267
|
+
if (!existsSync(folderPath)) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Cannot find templates at ${folderPath}. This is a bug in @grafana/create-plugin - please report it.`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const added = [];
|
|
273
|
+
for (const relativeFile of glob.sync("**", { cwd: folderPath, dot: true, nodir: true, posix: true })) {
|
|
274
|
+
const file = join(folderPath, relativeFile);
|
|
275
|
+
const outputFile = posix.join(posix.dirname(relativeFile), getExportFileName(file));
|
|
276
|
+
if (!include(outputFile)) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const filePath = join(targetPath, outputFile);
|
|
280
|
+
if (context.doesFileExist(filePath)) {
|
|
281
|
+
codemodsDebug(`${filePath} already exists, skipping`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
context.addFile(filePath, renderHandlebarsTemplate(readFileSync(file, "utf-8"), data));
|
|
285
|
+
added.push(filePath);
|
|
286
|
+
}
|
|
287
|
+
return added;
|
|
288
|
+
}
|
|
256
289
|
const warningComment = `/*
|
|
257
290
|
* \u26A0\uFE0F\u26A0\uFE0F\u26A0\uFE0F THIS FILE WAS SCAFFOLDED BY \`@grafana/create-plugin\`. DO NOT EDIT THIS FILE DIRECTLY. \u26A0\uFE0F\u26A0\uFE0F\u26A0\uFE0F
|
|
258
291
|
*
|
|
259
292
|
*/`;
|
|
260
293
|
|
|
261
|
-
export { addDependenciesToPackageJson, additionsDebug, flushChanges, formatFiles, getTemplateDataFromContext, installNPMDependencies, isVersionGreater, migrationsDebug, printChanges, readJsonFile, removeDependenciesFromPackageJson, renderTemplate, runGoModTidy };
|
|
294
|
+
export { addDependenciesToPackageJson, addTemplateFolder, additionsDebug, codemodsDebug, flushChanges, formatFiles, getTemplateDataFromContext, installNPMDependencies, isVersionGreater, migrationsDebug, printChanges, readJsonFile, removeDependenciesFromPackageJson, renderTemplate, runGoModTidy };
|