@grafana/create-plugin 7.10.0 → 7.11.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 +27 -0
- package/dist/codemods/additions/additions.js +5 -0
- package/dist/codemods/additions/scripts/experimental-app-sdk.js +275 -0
- package/dist/codemods/context.js +13 -1
- package/dist/codemods/migrations/migrations.js +6 -0
- package/dist/codemods/migrations/scripts/013-jest-esmodules.js +65 -0
- package/dist/codemods/runner.js +2 -1
- package/dist/codemods/utils.goMod.js +45 -0
- package/dist/codemods/utils.js +25 -1
- package/dist/commands/add.command.js +9 -4
- package/dist/commands/generate/print-success-message.js +1 -1
- package/package.json +3 -3
- package/src/codemods/additions/additions.ts +5 -0
- package/src/codemods/additions/scripts/experimental-app-sdk.test.ts +655 -0
- package/src/codemods/additions/scripts/experimental-app-sdk.ts +431 -0
- package/src/codemods/context.test.ts +7 -1
- package/src/codemods/context.ts +23 -1
- package/src/codemods/migrations/migrations.ts +7 -0
- package/src/codemods/migrations/scripts/013-jest-esmodules.test.ts +99 -0
- package/src/codemods/migrations/scripts/013-jest-esmodules.ts +79 -0
- package/src/codemods/runner.ts +3 -1
- package/src/codemods/utils.goMod.test.ts +86 -0
- package/src/codemods/utils.goMod.ts +70 -0
- package/src/codemods/utils.test.ts +49 -0
- package/src/codemods/utils.ts +39 -0
- package/src/commands/add.command.ts +10 -5
- package/src/commands/generate/print-success-message.ts +1 -1
- package/templates/app-sdk/.config/AGENTS/app-sdk.md +87 -0
- package/templates/app-sdk/.config/app-sdk/README.md +60 -0
- package/templates/app-sdk/.config/app-sdk/generate-kinds.mjs +138 -0
- package/templates/app-sdk/.github/workflows/generate-kinds-drift.yml +84 -0
- package/templates/app-sdk/kinds/README.md +3 -0
- package/templates/app-sdk/kinds/config.cue +39 -0
- package/templates/app-sdk/kinds/cue.mod/module.cue +4 -0
- package/templates/app-sdk/kinds/example.cue +29 -0
- package/templates/app-sdk/kinds/manifest.cue +29 -0
- package/templates/app-sdk/pkg/generated/example/v1alpha1/doc.go +11 -0
- package/templates/app-sdk/pkg/generated/manifestdata/doc.go +12 -0
- package/templates/app-sdk/pkg/provider/provider.go +30 -0
- package/templates/common/.config/jest/utils.js +2 -0
- package/templates/common/_package.json +1 -1
- package/vitest.setup.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [7.11.0](https://github.com/grafana/plugin-tools/compare/@grafana/create-plugin@7.10.1...@grafana/create-plugin@7.11.0) (2026-09-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **create-plugin:** add CI drift check workflow to experimental-app-sdk ([#2862](https://github.com/grafana/plugin-tools/issues/2862)) ([89b3a07](https://github.com/grafana/plugin-tools/commit/89b3a077b4ec9a9a85e58ceb514f7e94ee2ef21e))
|
|
9
|
+
* **create-plugin:** add grafana-app-sdk CUE kind code generation ([#2853](https://github.com/grafana/plugin-tools/issues/2853)) ([5f1a3c1](https://github.com/grafana/plugin-tools/commit/5f1a3c136b97ee451b8dae97fb65372f661ac589))
|
|
10
|
+
* **create-plugin:** extend experimental-app-sdk to Go backends ([#2856](https://github.com/grafana/plugin-tools/issues/2856)) ([79b6974](https://github.com/grafana/plugin-tools/commit/79b69741d24444de77f490368c816ea0f2efb951))
|
|
11
|
+
* **create-plugin:** run go mod tidy for codemods editing go.mod ([#2884](https://github.com/grafana/plugin-tools/issues/2884)) ([22ca6be](https://github.com/grafana/plugin-tools/commit/22ca6be4fc26e163e3a200baebb950cbd9958376))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **create-plugin:** Cleanup experimental-app-sdk message and formatting. ([#2883](https://github.com/grafana/plugin-tools/issues/2883)) ([707b2f0](https://github.com/grafana/plugin-tools/commit/707b2f0b7534fa01bdf1a14af4be0fe50ddc2131))
|
|
17
|
+
* **create-plugin:** defer add-command success message until after summary ([#2878](https://github.com/grafana/plugin-tools/issues/2878)) ([379d469](https://github.com/grafana/plugin-tools/commit/379d4691f188441ef801f9f49d2aa7d2ea8d202f))
|
|
18
|
+
* **create-plugin:** drop shebang from generate-kinds.mjs so it gets the do-not-edit header ([#2872](https://github.com/grafana/plugin-tools/issues/2872)) ([d1d25d9](https://github.com/grafana/plugin-tools/commit/d1d25d9f85ade3c7079ab04954daf23c413c8603))
|
|
19
|
+
* **create-plugin:** preserve 'add' changeType when updating a newly added file ([#2865](https://github.com/grafana/plugin-tools/issues/2865)) ([20e57ed](https://github.com/grafana/plugin-tools/commit/20e57ed48334e4e941d9fe3b6e07e370b36d2672))
|
|
20
|
+
* **deps:** Update dependency recast to v0.24.0 ([#2824](https://github.com/grafana/plugin-tools/issues/2824)) ([c57eba3](https://github.com/grafana/plugin-tools/commit/c57eba3601d70aee202f4786405d62cee38af264))
|
|
21
|
+
* **templates:** Update dependency @grafana/plugin-e2e to v3.12.0 ([#2870](https://github.com/grafana/plugin-tools/issues/2870)) ([c32a68c](https://github.com/grafana/plugin-tools/commit/c32a68c88a746df67400564100167aa65cd7dd3b))
|
|
22
|
+
|
|
23
|
+
## [7.10.1](https://github.com/grafana/plugin-tools/compare/@grafana/create-plugin@7.10.0...@grafana/create-plugin@7.10.1) (2026-08-27)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* **create-plugin:** add @react-hookz/web + @ver0/deep-equal to Jest ESM ignore ([#2847](https://github.com/grafana/plugin-tools/issues/2847)) ([66068ab](https://github.com/grafana/plugin-tools/commit/66068abc145224767470902386949fd8b65f6eaa))
|
|
29
|
+
|
|
3
30
|
## [7.10.0](https://github.com/grafana/plugin-tools/compare/@grafana/create-plugin@7.9.2...@grafana/create-plugin@7.10.0) (2026-08-21)
|
|
4
31
|
|
|
5
32
|
|
|
@@ -8,6 +8,11 @@ var defaultAdditions = [
|
|
|
8
8
|
name: "externalize-jsx-runtime",
|
|
9
9
|
description: "Externalizes the react JSX runtime to help migrate plugins to React 19",
|
|
10
10
|
scriptPath: import.meta.resolve("./scripts/externalize-jsx-runtime.js")
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: "experimental-app-sdk",
|
|
14
|
+
description: "Adds grafana-app-sdk CUE kind code generation to an app plugin",
|
|
15
|
+
scriptPath: import.meta.resolve("./scripts/experimental-app-sdk.js")
|
|
11
16
|
}
|
|
12
17
|
];
|
|
13
18
|
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { styleText } from 'node:util';
|
|
3
|
+
import { parseDocument, YAMLMap, Scalar, stringify } from 'yaml';
|
|
4
|
+
import { getBackendCmd } from '../../../commands/generate/print-success-message.js';
|
|
5
|
+
import { output } from '../../../utils/utils.console.js';
|
|
6
|
+
import { additionsDebug, renderTemplate } from '../../utils.js';
|
|
7
|
+
import { addRequireToGoMod } from '../../utils.goMod.js';
|
|
8
|
+
import { getTemplateData } from '../../../utils/utils.templates.js';
|
|
9
|
+
|
|
10
|
+
const APP_SDK_FEATURE_TOGGLES = ["appplugins.loadAppManifest", "appplugins.registerAPIServer"];
|
|
11
|
+
const TEMPLATE_FILES = [
|
|
12
|
+
[".config/app-sdk/generate-kinds.mjs", true],
|
|
13
|
+
[".config/app-sdk/README.md", false],
|
|
14
|
+
[".github/workflows/generate-kinds-drift.yml", false],
|
|
15
|
+
["kinds/config.cue", false],
|
|
16
|
+
["kinds/manifest.cue", false],
|
|
17
|
+
["kinds/example.cue", false],
|
|
18
|
+
["kinds/cue.mod/module.cue", false],
|
|
19
|
+
["kinds/README.md", false]
|
|
20
|
+
];
|
|
21
|
+
const APP_SDK_MD = ".config/AGENTS/app-sdk.md";
|
|
22
|
+
function appSdk(context) {
|
|
23
|
+
if (!isAppPlugin(context)) {
|
|
24
|
+
return context;
|
|
25
|
+
}
|
|
26
|
+
const changesBefore = Object.keys(context.listChanges()).length;
|
|
27
|
+
addTemplateFiles(context);
|
|
28
|
+
referenceAgentInstructions(context);
|
|
29
|
+
addGenerateScript(context);
|
|
30
|
+
addFeatureToggle(context);
|
|
31
|
+
wireGoBackend(context);
|
|
32
|
+
if (Object.keys(context.listChanges()).length > changesBefore) {
|
|
33
|
+
context.setMessage(buildNextStepsMessage(hasGoBackend(context)));
|
|
34
|
+
}
|
|
35
|
+
return context;
|
|
36
|
+
}
|
|
37
|
+
function isAppPlugin(context) {
|
|
38
|
+
const pluginJsonContent = context.getFile("src/plugin.json");
|
|
39
|
+
if (!pluginJsonContent) {
|
|
40
|
+
skip("Could not find src/plugin.json.", ["Run this from the root of your plugin."]);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
let pluginJson;
|
|
44
|
+
try {
|
|
45
|
+
pluginJson = JSON.parse(pluginJsonContent);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
additionsDebug(`Failed to parse src/plugin.json: ${error}`);
|
|
48
|
+
skip("Could not parse src/plugin.json.");
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (pluginJson.type !== "app") {
|
|
52
|
+
skip(`grafana-app-sdk codegen needs an app plugin, but this is a ${pluginJson.type} plugin.`, [
|
|
53
|
+
"The app-sdk serves Kubernetes-style resources from an app plugin."
|
|
54
|
+
]);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
function skip(title, body = []) {
|
|
60
|
+
output.warning({ title: `Skipping app-sdk: ${title}`, body });
|
|
61
|
+
}
|
|
62
|
+
function addTemplateFiles(context) {
|
|
63
|
+
for (const [file, includeWarning] of TEMPLATE_FILES) {
|
|
64
|
+
if (context.doesFileExist(file)) {
|
|
65
|
+
additionsDebug(`${file} already exists. Skipping.`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
context.addFile(file, renderTemplate(templatePath(file), includeWarning));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function templatePath(file) {
|
|
72
|
+
return fileURLToPath(new URL(`../../../../templates/app-sdk/${file}`, import.meta.url));
|
|
73
|
+
}
|
|
74
|
+
function referenceAgentInstructions(context) {
|
|
75
|
+
const path = ".config/AGENTS/instructions.md";
|
|
76
|
+
const instructions = context.getFile(path);
|
|
77
|
+
if (!instructions) {
|
|
78
|
+
additionsDebug(`Could not find ${path}. Skipping the app-sdk reference.`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!context.doesFileExist(APP_SDK_MD)) {
|
|
82
|
+
context.addFile(APP_SDK_MD, renderTemplate(templatePath(APP_SDK_MD), false));
|
|
83
|
+
}
|
|
84
|
+
if (instructions.includes(APP_SDK_MD)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
context.updateFile(
|
|
88
|
+
path,
|
|
89
|
+
`${instructions.trimEnd()}
|
|
90
|
+
- 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.**
|
|
91
|
+
`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
function addGenerateScript(context) {
|
|
95
|
+
const raw = context.getFile("package.json");
|
|
96
|
+
if (!raw) {
|
|
97
|
+
additionsDebug("Could not find package.json. Skipping the generate:kinds script.");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
let packageJson;
|
|
101
|
+
try {
|
|
102
|
+
packageJson = JSON.parse(raw);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
additionsDebug(`Failed to parse package.json: ${error}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (packageJson.scripts?.["generate:kinds"]) {
|
|
108
|
+
additionsDebug("A generate:kinds script already exists. Skipping.");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
packageJson.scripts = { ...packageJson.scripts, "generate:kinds": "node ./.config/app-sdk/generate-kinds.mjs" };
|
|
112
|
+
context.updateFile("package.json", JSON.stringify(packageJson, null, 2));
|
|
113
|
+
}
|
|
114
|
+
function addFeatureToggle(context) {
|
|
115
|
+
const composePath = "docker-compose.yaml";
|
|
116
|
+
const composeContent = context.getFile(composePath);
|
|
117
|
+
if (!composeContent) {
|
|
118
|
+
additionsDebug(`Could not find ${composePath}. Skipping the feature toggle.`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const baseComposeContent = context.getFile(".config/docker-compose-base.yaml");
|
|
122
|
+
if (APP_SDK_FEATURE_TOGGLES.every((toggle) => baseComposeContent?.includes(toggle))) {
|
|
123
|
+
additionsDebug(`${APP_SDK_FEATURE_TOGGLES.join(", ")} are already enabled in the base compose file. Skipping.`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const composeData = parseDocument(composeContent);
|
|
127
|
+
const environment = composeData.getIn(["services", "grafana", "environment"]);
|
|
128
|
+
if (environment !== void 0 && !(environment instanceof YAMLMap)) {
|
|
129
|
+
additionsDebug(
|
|
130
|
+
`services.grafana.environment in ${composePath} is not a mapping. Add ${APP_SDK_FEATURE_TOGGLES.join(", ")} to GF_FEATURE_TOGGLES_ENABLE manually.`
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const grafanaService = composeData.getIn(["services", "grafana"]);
|
|
135
|
+
if (!(grafanaService instanceof YAMLMap)) {
|
|
136
|
+
additionsDebug(`Could not find the grafana service in ${composePath}. Skipping the feature toggle.`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const existing = composeData.getIn(["services", "grafana", "environment", "GF_FEATURE_TOGGLES_ENABLE"], true);
|
|
140
|
+
const existingValue = existing instanceof Scalar ? String(existing.value) : void 0;
|
|
141
|
+
const toggles = existingValue ? existingValue.split(",").map((toggle) => toggle.trim()).filter(Boolean) : [];
|
|
142
|
+
const missingToggles = APP_SDK_FEATURE_TOGGLES.filter((toggle) => !toggles.includes(toggle));
|
|
143
|
+
if (missingToggles.length === 0) {
|
|
144
|
+
additionsDebug(`${APP_SDK_FEATURE_TOGGLES.join(", ")} are already enabled. Skipping.`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
toggles.push(...missingToggles);
|
|
148
|
+
composeData.setIn(["services", "grafana", "environment", "GF_FEATURE_TOGGLES_ENABLE"], toggles.join(","));
|
|
149
|
+
context.updateFile(composePath, stringify(composeData, { lineWidth: 120, singleQuote: true }));
|
|
150
|
+
}
|
|
151
|
+
function wireGoBackend(context) {
|
|
152
|
+
if (!hasGoBackend(context)) {
|
|
153
|
+
additionsDebug("No Go backend found. Skipping main.go wiring.");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
addAppProvider(context);
|
|
157
|
+
addGeneratedStubs(context);
|
|
158
|
+
wireMainGo(context);
|
|
159
|
+
addGoModDependency(context);
|
|
160
|
+
}
|
|
161
|
+
const GRAFANA_APP_SDK_VERSION = "v0.60.0";
|
|
162
|
+
function addGoModDependency(context) {
|
|
163
|
+
addRequireToGoMod(context, "github.com/grafana/grafana-app-sdk", GRAFANA_APP_SDK_VERSION);
|
|
164
|
+
addRequireToGoMod(context, "github.com/grafana/grafana-app-sdk/plugin", GRAFANA_APP_SDK_VERSION);
|
|
165
|
+
}
|
|
166
|
+
function addAppProvider(context) {
|
|
167
|
+
const path = "pkg/provider/provider.go";
|
|
168
|
+
if (context.doesFileExist(path)) {
|
|
169
|
+
additionsDebug(`${path} already exists. Skipping.`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
context.addFile(path, renderTemplate(templatePath(path), false));
|
|
173
|
+
}
|
|
174
|
+
const GENERATED_STUB_PATHS = ["pkg/generated/example/v1alpha1/doc.go", "pkg/generated/manifestdata/doc.go"];
|
|
175
|
+
function addGeneratedStubs(context) {
|
|
176
|
+
for (const path of GENERATED_STUB_PATHS) {
|
|
177
|
+
if (context.doesFileExist(path)) {
|
|
178
|
+
additionsDebug(`${path} already exists. Skipping.`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
context.addFile(path, renderTemplate(templatePath(path), false));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function hasGoBackend(context) {
|
|
185
|
+
const pluginJsonContent = context.getFile("src/plugin.json");
|
|
186
|
+
if (!pluginJsonContent) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(pluginJsonContent).backend === true;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
additionsDebug(`Failed to parse src/plugin.json: ${error}`);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const APP_MANAGE_STATEMENT_REGEX = /if err := app\.Manage\((".*?"), (\S+), app\.ManageOpts\{\}\); err != nil \{\n(\t+[\s\S]*?\n)\t\}/;
|
|
197
|
+
function wireMainGo(context) {
|
|
198
|
+
const path = "pkg/main.go";
|
|
199
|
+
const content = context.getFile(path);
|
|
200
|
+
if (!content) {
|
|
201
|
+
additionsDebug(`Could not find ${path}. Skipping the app-sdk backend wiring.`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (content.includes('grafana-app-sdk/plugin"')) {
|
|
205
|
+
additionsDebug(`${path} already wires plugin.Run. Skipping.`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const match = content.match(APP_MANAGE_STATEMENT_REGEX);
|
|
209
|
+
if (!match) {
|
|
210
|
+
skip(`${path} does not match the expected app.Manage(...) call.`, [
|
|
211
|
+
"Wire the grafana-app-sdk plugin.Run helper into main.go yourself:",
|
|
212
|
+
"https://github.com/grafana/grafana-app-sdk/blob/main/plugin/run.go"
|
|
213
|
+
]);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const [fullStatement, pluginId, appFactory, errorBody] = match;
|
|
217
|
+
const moduleMatch = content.match(/"(github\.com\/[^/]+\/[^/]+)\/pkg\/plugin"/);
|
|
218
|
+
const providerImportPath = moduleMatch ? `${moduleMatch[1]}/pkg/provider` : void 0;
|
|
219
|
+
if (!providerImportPath) {
|
|
220
|
+
skip(`${path} does not import its own pkg/plugin package under a recognisable module path.`, [
|
|
221
|
+
"Wire the grafana-app-sdk plugin.Run helper into main.go yourself:",
|
|
222
|
+
"https://github.com/grafana/grafana-app-sdk/blob/main/plugin/run.go"
|
|
223
|
+
]);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const appImport = ' "github.com/grafana/grafana-plugin-sdk-go/backend/app"';
|
|
227
|
+
const pluginImport = `"${moduleMatch[1]}/pkg/plugin"`;
|
|
228
|
+
if (!content.includes(appImport) || !content.includes(pluginImport)) {
|
|
229
|
+
skip(`${path} does not match the expected import shape.`, [
|
|
230
|
+
"Wire the grafana-app-sdk plugin.Run helper into main.go yourself:",
|
|
231
|
+
"https://github.com/grafana/grafana-app-sdk/blob/main/plugin/run.go"
|
|
232
|
+
]);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const updated = content.replace(appImport, ' sdkplugin "github.com/grafana/grafana-app-sdk/plugin"').replace(pluginImport, `${pluginImport}
|
|
236
|
+
"${providerImportPath}"`).replace(
|
|
237
|
+
fullStatement,
|
|
238
|
+
`if err := sdkplugin.Run(
|
|
239
|
+
provider.New(),
|
|
240
|
+
sdkplugin.WithPluginID(${pluginId}),
|
|
241
|
+
sdkplugin.WithAppFunc(${appFactory}),
|
|
242
|
+
); err != nil {
|
|
243
|
+
${errorBody} }`
|
|
244
|
+
);
|
|
245
|
+
context.updateFile(path, updated);
|
|
246
|
+
}
|
|
247
|
+
function buildNextStepsMessage(hasGoBackend2) {
|
|
248
|
+
const { packageManagerName } = getTemplateData();
|
|
249
|
+
const versionBadge = styleText(["reset", "inverse", "bold", "cyan"], ` grafana-app-sdk@${GRAFANA_APP_SDK_VERSION} `);
|
|
250
|
+
const commands = output.bulletList([
|
|
251
|
+
`${output.formatCode(`${packageManagerName} run generate:kinds`)} ${styleText(["dim"], "to generate code from the definitions in kinds/")}`,
|
|
252
|
+
...hasGoBackend2 ? [
|
|
253
|
+
// The generated Go code under pkg/generated/ only takes effect once it's compiled into the
|
|
254
|
+
// backend binary.
|
|
255
|
+
`${getBackendCmd()} ${styleText(["dim"], "to rebuild the plugin backend with the generated code")}`
|
|
256
|
+
] : [],
|
|
257
|
+
// Grafana reads the app manifest from the bundle at startup, so a manifest change (any change to
|
|
258
|
+
// the kinds) isn't picked up by an already-running Grafana instance until it's restarted.
|
|
259
|
+
`${output.formatCode("docker compose restart grafana")} ${styleText(["dim"], "to pick up changes to kinds/")}`
|
|
260
|
+
]);
|
|
261
|
+
return {
|
|
262
|
+
level: "success",
|
|
263
|
+
title: "Successfully added grafana-app-sdk code generation to your plugin.",
|
|
264
|
+
body: [
|
|
265
|
+
`${versionBadge} ${styleText(["cyan", "bold"], "Next steps:")}`,
|
|
266
|
+
"",
|
|
267
|
+
"Run the following commands to get started:",
|
|
268
|
+
...commands,
|
|
269
|
+
"",
|
|
270
|
+
`See ${output.formatCode("./.config/app-sdk/README.md")} for the full workflow.`
|
|
271
|
+
]
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export { appSdk as default };
|
package/dist/codemods/context.js
CHANGED
|
@@ -9,9 +9,20 @@ const codemodsDebug = debug.extend("codemods");
|
|
|
9
9
|
class Context {
|
|
10
10
|
constructor(basePath) {
|
|
11
11
|
__publicField(this, "files", {});
|
|
12
|
+
__publicField(this, "message");
|
|
12
13
|
__publicField(this, "basePath");
|
|
13
14
|
this.basePath = basePath || process.cwd();
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Lets a codemod defer printing a message until after the caller has reported success, instead
|
|
18
|
+
* of printing immediately during the codemod's own execution.
|
|
19
|
+
*/
|
|
20
|
+
setMessage(message) {
|
|
21
|
+
this.message = message;
|
|
22
|
+
}
|
|
23
|
+
getMessage() {
|
|
24
|
+
return this.message;
|
|
25
|
+
}
|
|
15
26
|
addFile(filePath, content) {
|
|
16
27
|
const path = this.normalisePath(filePath);
|
|
17
28
|
if (!this.doesFileExist(path)) {
|
|
@@ -40,7 +51,8 @@ class Context {
|
|
|
40
51
|
throw new Error(`File ${path} does not exist`);
|
|
41
52
|
}
|
|
42
53
|
if (originalContent !== content) {
|
|
43
|
-
this.files[path]
|
|
54
|
+
const changeType = this.files[path]?.changeType === "add" ? "add" : "update";
|
|
55
|
+
this.files[path] = { content, changeType };
|
|
44
56
|
} else {
|
|
45
57
|
codemodsDebug(`Context.updateFile() - no updates for ${filePath}`);
|
|
46
58
|
}
|
|
@@ -72,6 +72,12 @@ var defaultMigrations = [
|
|
|
72
72
|
version: "7.8.2",
|
|
73
73
|
description: "Emit a single LICENSE.txt file for all licenses in bundled code.",
|
|
74
74
|
scriptPath: import.meta.resolve("./scripts/012-terser-license-file.js")
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "013-jest-esmodules",
|
|
78
|
+
version: "7.10.1",
|
|
79
|
+
description: "Support @grafana/ui@13.2.0 in Jest by transforming @react-hookz/web and @ver0/deep-equal ESM dependencies.",
|
|
80
|
+
scriptPath: import.meta.resolve("./scripts/013-jest-esmodules.js")
|
|
75
81
|
}
|
|
76
82
|
// Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
|
|
77
83
|
// for those written before the switch to updates as migrations.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import * as recast from 'recast';
|
|
3
|
+
import { migrationsDebug } from '../../utils.js';
|
|
4
|
+
import { parseAsTypescript, findVariableDeclaration, printAST } from '../../utils.ast.js';
|
|
5
|
+
|
|
6
|
+
const { builders } = recast.types;
|
|
7
|
+
const JEST_UTILS_PATH = join(".config", "jest", "utils.js");
|
|
8
|
+
const ESM_MODULES_TO_ADD = ["@react-hookz/web", "@ver0/deep-equal"];
|
|
9
|
+
function migrate(context) {
|
|
10
|
+
if (!context.doesFileExist(JEST_UTILS_PATH)) {
|
|
11
|
+
migrationsDebug(`${JEST_UTILS_PATH} not found. Skipping Jest ESM modules migration.`);
|
|
12
|
+
return context;
|
|
13
|
+
}
|
|
14
|
+
const source = context.getFile(JEST_UTILS_PATH);
|
|
15
|
+
if (!source) {
|
|
16
|
+
migrationsDebug(`${JEST_UTILS_PATH} is empty. Skipping Jest ESM modules migration.`);
|
|
17
|
+
return context;
|
|
18
|
+
}
|
|
19
|
+
const parsed = parseAsTypescript(source);
|
|
20
|
+
if (!parsed.success) {
|
|
21
|
+
migrationsDebug(`Failed to parse ${JEST_UTILS_PATH}. Error: ${parsed.error.message}`);
|
|
22
|
+
return context;
|
|
23
|
+
}
|
|
24
|
+
const grafanaESModules = findVariableDeclaration(parsed.ast, "grafanaESModules");
|
|
25
|
+
if (!grafanaESModules) {
|
|
26
|
+
migrationsDebug(`Could not find grafanaESModules variable declaration in ${JEST_UTILS_PATH}`);
|
|
27
|
+
return context;
|
|
28
|
+
}
|
|
29
|
+
if (grafanaESModules.init?.type !== "ArrayExpression") {
|
|
30
|
+
migrationsDebug(`grafanaESModules variable in ${JEST_UTILS_PATH} is not an array.`);
|
|
31
|
+
return context;
|
|
32
|
+
}
|
|
33
|
+
const existingModules = new Set(
|
|
34
|
+
grafanaESModules.init.elements.map(getStringValue).filter((value) => value !== void 0)
|
|
35
|
+
);
|
|
36
|
+
const missingModules = ESM_MODULES_TO_ADD.filter((moduleName) => !existingModules.has(moduleName));
|
|
37
|
+
if (missingModules.length === 0) {
|
|
38
|
+
return context;
|
|
39
|
+
}
|
|
40
|
+
const schemaIndex = grafanaESModules.init.elements.findIndex(
|
|
41
|
+
(element) => getStringValue(element) === "@grafana/schema"
|
|
42
|
+
);
|
|
43
|
+
const insertIndex = schemaIndex === -1 ? grafanaESModules.init.elements.length : schemaIndex + 1;
|
|
44
|
+
grafanaESModules.init.elements.splice(
|
|
45
|
+
insertIndex,
|
|
46
|
+
0,
|
|
47
|
+
...missingModules.map((moduleName) => builders.literal(moduleName))
|
|
48
|
+
);
|
|
49
|
+
context.updateFile(JEST_UTILS_PATH, printAST(parsed.ast));
|
|
50
|
+
return context;
|
|
51
|
+
}
|
|
52
|
+
function getStringValue(element) {
|
|
53
|
+
if (!element) {
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
if (element.type === "Literal" && typeof element.value === "string") {
|
|
57
|
+
return element.value;
|
|
58
|
+
}
|
|
59
|
+
if (element.type === "StringLiteral") {
|
|
60
|
+
return element.value;
|
|
61
|
+
}
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { migrate as default };
|
package/dist/codemods/runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context } from './context.js';
|
|
2
|
-
import { formatFiles, flushChanges, printChanges, installNPMDependencies } from './utils.js';
|
|
2
|
+
import { formatFiles, flushChanges, printChanges, installNPMDependencies, runGoModTidy } from './utils.js';
|
|
3
3
|
import { parseAndValidateOptions } from './schema-parser.js';
|
|
4
4
|
|
|
5
5
|
async function runCodemod(codemod, options) {
|
|
@@ -19,6 +19,7 @@ async function runCodemod(codemod, options) {
|
|
|
19
19
|
flushChanges(updatedContext);
|
|
20
20
|
printChanges(updatedContext, codemod.name, codemod.description);
|
|
21
21
|
installNPMDependencies(updatedContext);
|
|
22
|
+
runGoModTidy(updatedContext);
|
|
22
23
|
return updatedContext;
|
|
23
24
|
} catch (error) {
|
|
24
25
|
if (error instanceof Error) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { migrationsDebug, isVersionGreater } from './utils.js';
|
|
2
|
+
|
|
3
|
+
function singleLineRequireRegex(modulePath) {
|
|
4
|
+
const escapedModulePath = modulePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
|
+
return new RegExp(`^require ${escapedModulePath} (\\S+)(.*)$`, "m");
|
|
6
|
+
}
|
|
7
|
+
function addRequireToGoMod(context, modulePath, version, goModPath = "go.mod") {
|
|
8
|
+
const content = context.getFile(goModPath);
|
|
9
|
+
if (!content) {
|
|
10
|
+
migrationsDebug(`Could not find ${goModPath}. Skipping the ${modulePath} dependency.`);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const requireRegex = singleLineRequireRegex(modulePath);
|
|
14
|
+
const match = content.match(requireRegex);
|
|
15
|
+
if (match) {
|
|
16
|
+
const [, existingVersion] = match;
|
|
17
|
+
if (!isVersionGreater(version, existingVersion, false)) {
|
|
18
|
+
migrationsDebug(`${goModPath} already requires ${modulePath} at ${existingVersion}. Skipping.`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const updated = content.replace(requireRegex, `require ${modulePath} ${version}$2`);
|
|
22
|
+
context.updateFile(goModPath, updated);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const insertAt = findRequireInsertionPoint(content);
|
|
26
|
+
const newRequireLine = `require ${modulePath} ${version}
|
|
27
|
+
`;
|
|
28
|
+
context.updateFile(goModPath, `${content.slice(0, insertAt)}${newRequireLine}${content.slice(insertAt)}`);
|
|
29
|
+
}
|
|
30
|
+
function findRequireInsertionPoint(content) {
|
|
31
|
+
const requireLines = [...content.matchAll(/^require \S+ \S+.*$/gm)];
|
|
32
|
+
const lastRequireLine = requireLines[requireLines.length - 1];
|
|
33
|
+
if (lastRequireLine) {
|
|
34
|
+
const lineEnd = content.indexOf("\n", lastRequireLine.index + lastRequireLine[0].length);
|
|
35
|
+
return lineEnd === -1 ? content.length : lineEnd + 1;
|
|
36
|
+
}
|
|
37
|
+
const goDirective = content.match(/^go \d+\.\d+(\.\d+)?.*$/m);
|
|
38
|
+
if (goDirective) {
|
|
39
|
+
const lineEnd = content.indexOf("\n", goDirective.index + goDirective[0].length);
|
|
40
|
+
return lineEnd === -1 ? content.length : lineEnd + 1;
|
|
41
|
+
}
|
|
42
|
+
return content.length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { addRequireToGoMod };
|
package/dist/codemods/utils.js
CHANGED
|
@@ -5,6 +5,7 @@ import { styleText } from 'node:util';
|
|
|
5
5
|
import { output } from '../utils/utils.console.js';
|
|
6
6
|
import { getPackageManagerWithFallback, getPackageManagerSilentInstallCmd } from '../utils/utils.packageManager.js';
|
|
7
7
|
import { execSync } from 'node:child_process';
|
|
8
|
+
import which from 'which';
|
|
8
9
|
import { gte, gt, clean, coerce } from 'semver';
|
|
9
10
|
import { debug } from '../utils/utils.cli.js';
|
|
10
11
|
import { renderHandlebarsTemplate } from '../utils/utils.handlebars.js';
|
|
@@ -103,6 +104,29 @@ function installNPMDependencies(context) {
|
|
|
103
104
|
execSync(installCmd, { cwd: context.basePath, stdio: "inherit" });
|
|
104
105
|
}
|
|
105
106
|
}
|
|
107
|
+
let goModTidyCache;
|
|
108
|
+
function runGoModTidy(context) {
|
|
109
|
+
const hasGoModChanges = Object.entries(context.listChanges()).some(
|
|
110
|
+
([filePath, { changeType }]) => filePath === "go.mod" && changeType === "update"
|
|
111
|
+
);
|
|
112
|
+
if (!hasGoModChanges) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const goModContents = context.getFile("go.mod");
|
|
116
|
+
if (!goModContents) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (goModContents === goModTidyCache) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!which.sync("go", { nothrow: true })) {
|
|
123
|
+
additionsDebug("No `go` binary found on PATH. Skipping `go mod tidy`.");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
goModTidyCache = goModContents;
|
|
127
|
+
output.logSingleLine("Running `go mod tidy`...");
|
|
128
|
+
execSync("go mod tidy", { cwd: context.basePath, stdio: "inherit" });
|
|
129
|
+
}
|
|
106
130
|
function readJsonFile(context, path) {
|
|
107
131
|
if (!context.doesFileExist(path)) {
|
|
108
132
|
throw new Error(`Cannot find ${path}`);
|
|
@@ -224,4 +248,4 @@ const warningComment = `/*
|
|
|
224
248
|
*
|
|
225
249
|
*/`;
|
|
226
250
|
|
|
227
|
-
export { addDependenciesToPackageJson, additionsDebug, flushChanges, formatFiles, installNPMDependencies, isVersionGreater, migrationsDebug, printChanges, readJsonFile, removeDependenciesFromPackageJson, renderTemplate };
|
|
251
|
+
export { addDependenciesToPackageJson, additionsDebug, flushChanges, formatFiles, installNPMDependencies, isVersionGreater, migrationsDebug, printChanges, readJsonFile, removeDependenciesFromPackageJson, renderTemplate, runGoModTidy };
|
|
@@ -20,10 +20,15 @@ const add = async (argv) => {
|
|
|
20
20
|
Available additions: ${additionsList.join(", ")}`);
|
|
21
21
|
}
|
|
22
22
|
const { _, $0, ...codemodOptions } = argv;
|
|
23
|
-
await runCodemod(addition, codemodOptions);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const context = await runCodemod(addition, codemodOptions);
|
|
24
|
+
const message = context.getMessage();
|
|
25
|
+
if (message) {
|
|
26
|
+
output[message.level]({ title: message.title, body: message.body });
|
|
27
|
+
} else {
|
|
28
|
+
output.success({
|
|
29
|
+
title: `Successfully added ${addition.name} to your plugin.`
|
|
30
|
+
});
|
|
31
|
+
}
|
|
27
32
|
} catch (error) {
|
|
28
33
|
if (error instanceof Error) {
|
|
29
34
|
output.error({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grafana/create-plugin",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.11.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"directory": "packages/create-plugin",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"handlebars": "4.7.9",
|
|
36
36
|
"jsonc-parser": "3.3.1",
|
|
37
37
|
"minimist": "1.2.8",
|
|
38
|
-
"recast": "0.
|
|
38
|
+
"recast": "0.24.0",
|
|
39
39
|
"semver": "7.8.5",
|
|
40
40
|
"title-case": "4.3.2",
|
|
41
41
|
"valibot": "1.4.2",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@libs/version": "1.0.2",
|
|
49
49
|
"@types/glob": "9.0.0",
|
|
50
50
|
"@types/minimist": "1.2.5",
|
|
51
|
-
"@types/semver": "7.
|
|
51
|
+
"@types/semver": "7.8.0",
|
|
52
52
|
"@types/tmp": "0.2.6",
|
|
53
53
|
"@types/which": "3.0.4",
|
|
54
54
|
"tmp": "0.2.7"
|
|
@@ -11,4 +11,9 @@ export default [
|
|
|
11
11
|
description: 'Externalizes the react JSX runtime to help migrate plugins to React 19',
|
|
12
12
|
scriptPath: import.meta.resolve('./scripts/externalize-jsx-runtime.js'),
|
|
13
13
|
},
|
|
14
|
+
{
|
|
15
|
+
name: 'experimental-app-sdk',
|
|
16
|
+
description: 'Adds grafana-app-sdk CUE kind code generation to an app plugin',
|
|
17
|
+
scriptPath: import.meta.resolve('./scripts/experimental-app-sdk.js'),
|
|
18
|
+
},
|
|
14
19
|
] satisfies Codemod[];
|