@grafana/create-plugin 7.10.1 → 7.12.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/codemods/additions/additions.js +5 -0
  3. package/dist/codemods/additions/scripts/experimental-app-sdk.js +275 -0
  4. package/dist/codemods/context.js +13 -1
  5. package/dist/codemods/migrations/migrations.js +6 -0
  6. package/dist/codemods/migrations/scripts/014-eslint-config-v10.js +35 -0
  7. package/dist/codemods/runner.js +2 -1
  8. package/dist/codemods/utils.goMod.js +45 -0
  9. package/dist/codemods/utils.js +25 -1
  10. package/dist/commands/add.command.js +9 -4
  11. package/dist/commands/generate/print-success-message.js +1 -1
  12. package/dist/utils/utils.templates.js +2 -2
  13. package/package.json +3 -3
  14. package/src/codemods/additions/additions.ts +5 -0
  15. package/src/codemods/additions/scripts/experimental-app-sdk.test.ts +655 -0
  16. package/src/codemods/additions/scripts/experimental-app-sdk.ts +431 -0
  17. package/src/codemods/context.test.ts +7 -1
  18. package/src/codemods/context.ts +23 -1
  19. package/src/codemods/migrations/migrations.ts +7 -0
  20. package/src/codemods/migrations/scripts/014-eslint-config-v10.test.ts +94 -0
  21. package/src/codemods/migrations/scripts/014-eslint-config-v10.ts +40 -0
  22. package/src/codemods/runner.ts +3 -1
  23. package/src/codemods/utils.goMod.test.ts +86 -0
  24. package/src/codemods/utils.goMod.ts +70 -0
  25. package/src/codemods/utils.test.ts +49 -0
  26. package/src/codemods/utils.ts +39 -0
  27. package/src/commands/add.command.ts +10 -5
  28. package/src/commands/generate/print-success-message.ts +1 -1
  29. package/src/utils/utils.templates.ts +2 -2
  30. package/templates/app/src/plugin.json +1 -1
  31. package/templates/app-sdk/.config/AGENTS/app-sdk.md +99 -0
  32. package/templates/app-sdk/.config/app-sdk/README.md +126 -0
  33. package/templates/app-sdk/.config/app-sdk/generate-kinds.mjs +138 -0
  34. package/templates/app-sdk/.github/workflows/generate-kinds-drift.yml +84 -0
  35. package/templates/app-sdk/kinds/README.md +3 -0
  36. package/templates/app-sdk/kinds/config.cue +39 -0
  37. package/templates/app-sdk/kinds/cue.mod/module.cue +4 -0
  38. package/templates/app-sdk/kinds/example.cue +32 -0
  39. package/templates/app-sdk/kinds/manifest.cue +53 -0
  40. package/templates/app-sdk/pkg/generated/example/v1alpha1/doc.go +11 -0
  41. package/templates/app-sdk/pkg/generated/manifestdata/doc.go +12 -0
  42. package/templates/app-sdk/pkg/provider/provider.go +30 -0
  43. package/templates/common/.config/docker-compose-base.yaml +1 -1
  44. package/templates/common/.config/eslint.config.mjs +1 -1
  45. package/templates/common/.config/jest-setup.js +3 -1
  46. package/templates/common/_package.json +13 -13
  47. package/templates/datasource/src/plugin.json +1 -1
  48. package/templates/panel/src/plugin.json +1 -1
  49. package/templates/scenes-app/src/plugin.json +1 -1
  50. package/vitest.setup.ts +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.12.0](https://github.com/grafana/plugin-tools/compare/@grafana/create-plugin@7.11.0...@grafana/create-plugin@7.12.0) (2026-09-22)
4
+
5
+
6
+ ### Features
7
+
8
+ * **deps:** bump grafana libs to latest in templates in create plugin ([#2893](https://github.com/grafana/plugin-tools/issues/2893)) ([d3c3f4b](https://github.com/grafana/plugin-tools/commit/d3c3f4b2a775d9bfcb0a9209a0c62a2ef16bbd58))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **create-plugin:** Improve experimental-app-sdk templates with RBAC guidance. ([#2892](https://github.com/grafana/plugin-tools/issues/2892)) ([ec59abe](https://github.com/grafana/plugin-tools/commit/ec59abe7f7508e6dc206a24a0658d121d64b3f1f))
14
+ * **create-plugin:** support @grafana/eslint-config v10 in scaffold ([#2873](https://github.com/grafana/plugin-tools/issues/2873)) ([67f6fec](https://github.com/grafana/plugin-tools/commit/67f6fec1321943ae8062e130745b37f3e45c1d72))
15
+
16
+ ## [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)
17
+
18
+
19
+ ### Features
20
+
21
+ * **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))
22
+ * **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))
23
+ * **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))
24
+ * **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))
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * **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))
30
+ * **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))
31
+ * **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))
32
+ * **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))
33
+ * **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))
34
+ * **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))
35
+
3
36
  ## [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)
4
37
 
5
38
 
@@ -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 };
@@ -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] = { content, changeType: "update" };
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
  }
@@ -78,6 +78,12 @@ var defaultMigrations = [
78
78
  version: "7.10.1",
79
79
  description: "Support @grafana/ui@13.2.0 in Jest by transforming @react-hookz/web and @ver0/deep-equal ESM dependencies.",
80
80
  scriptPath: import.meta.resolve("./scripts/013-jest-esmodules.js")
81
+ },
82
+ {
83
+ name: "014-eslint-config-v10",
84
+ version: "7.11.1",
85
+ description: "Update @grafana/eslint-config to v10: drop the removed ./flat.js subpath import and swap @stylistic/eslint-plugin-ts for @stylistic/eslint-plugin.",
86
+ scriptPath: import.meta.resolve("./scripts/014-eslint-config-v10.js")
81
87
  }
82
88
  // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
83
89
  // for those written before the switch to updates as migrations.
@@ -0,0 +1,35 @@
1
+ import { isVersionGreater, addDependenciesToPackageJson, removeDependenciesFromPackageJson } from '../../utils.js';
2
+
3
+ const OLD_IMPORT = "import grafanaConfig from '@grafana/eslint-config/flat.js';";
4
+ const NEW_IMPORT = "import grafanaConfig from '@grafana/eslint-config';";
5
+ const ESLINT_CONFIG_PATH = ".config/eslint.config.mjs";
6
+ function migrate(context) {
7
+ if (!context.doesFileExist("package.json")) {
8
+ return context;
9
+ }
10
+ const packageJson = JSON.parse(context.getFile("package.json") || "{}");
11
+ const currentVersion = packageJson.devDependencies?.["@grafana/eslint-config"];
12
+ if (!currentVersion || !isVersionGreater("^10.0.0", currentVersion)) {
13
+ return context;
14
+ }
15
+ if (context.doesFileExist(ESLINT_CONFIG_PATH)) {
16
+ const content = context.getFile(ESLINT_CONFIG_PATH) || "";
17
+ if (content.includes(OLD_IMPORT)) {
18
+ context.updateFile(ESLINT_CONFIG_PATH, content.replace(OLD_IMPORT, NEW_IMPORT));
19
+ }
20
+ }
21
+ addDependenciesToPackageJson(
22
+ context,
23
+ {},
24
+ {
25
+ "@grafana/eslint-config": "^10.0.0",
26
+ "@stylistic/eslint-plugin": "^5.10.0",
27
+ "eslint-config-prettier": "^10.1.0",
28
+ "eslint-plugin-react-hooks": "^7.1.0"
29
+ }
30
+ );
31
+ removeDependenciesFromPackageJson(context, [], ["@stylistic/eslint-plugin-ts"]);
32
+ return context;
33
+ }
34
+
35
+ export { migrate as default };
@@ -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 };
@@ -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
- output.success({
25
- title: `Successfully added ${addition.name} to your plugin.`
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({
@@ -41,4 +41,4 @@ function getBackendCmd() {
41
41
  return output.formatCode("mage -v build:linux");
42
42
  }
43
43
 
44
- export { printGenerateSuccessMessage };
44
+ export { getBackendCmd, printGenerateSuccessMessage };
@@ -78,7 +78,7 @@ function getTemplateData(cliArgs) {
78
78
  isNPM: isNPM(packageManagerName),
79
79
  version: currentVersion,
80
80
  bundleGrafanaUI,
81
- scenesVersion: "^7.0.0",
81
+ scenesVersion: "^8.17.0",
82
82
  useExperimentalRspack: Boolean(features.useExperimentalRspack),
83
83
  frontendBundler
84
84
  };
@@ -99,7 +99,7 @@ function getTemplateData(cliArgs) {
99
99
  isNPM: isNPM(packageManagerName),
100
100
  version: currentVersion,
101
101
  bundleGrafanaUI,
102
- scenesVersion: "^7.0.0",
102
+ scenesVersion: "^8.17.0",
103
103
  pluginExecutable: pluginJson.executable,
104
104
  useExperimentalRspack: Boolean(features.useExperimentalRspack),
105
105
  frontendBundler
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/create-plugin",
3
- "version": "7.10.1",
3
+ "version": "7.12.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.23.12",
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.7.1",
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[];