@grafana/create-plugin 7.8.1 → 7.9.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.9.0](https://github.com/grafana/plugin-tools/compare/@grafana/create-plugin@7.8.1...@grafana/create-plugin@7.9.0) (2026-07-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * **deps:** bump grafana libs to 13.1.0 in templates ([#2750](https://github.com/grafana/plugin-tools/issues/2750)) ([dab0f70](https://github.com/grafana/plugin-tools/commit/dab0f70ec9816b56d6fe73230fd24d0e2b76b03e))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * output single license file when building plugin ([#2770](https://github.com/grafana/plugin-tools/issues/2770)) ([fd20ced](https://github.com/grafana/plugin-tools/commit/fd20cedfffc053e69898081740038544db2c7470))
14
+
3
15
  ## [7.8.1](https://github.com/grafana/plugin-tools/compare/@grafana/create-plugin@7.8.0...@grafana/create-plugin@7.8.1) (2026-07-09)
4
16
 
5
17
 
@@ -66,6 +66,12 @@ var defaultMigrations = [
66
66
  version: "7.6.3",
67
67
  description: "Security: replace insecure inline `npx --yes @grafana/sign-plugin@latest` sign script with a locked @grafana/sign-plugin devDependency to prevent arbitrary code execution from a compromised @latest publish.",
68
68
  scriptPath: import.meta.resolve("./scripts/011-secure-sign-script.js")
69
+ },
70
+ {
71
+ name: "012-terser-license-file",
72
+ version: "7.8.2",
73
+ description: "Emit a single LICENSE.txt file for all licenses in bundled code.",
74
+ scriptPath: import.meta.resolve("./scripts/012-terser-license-file.js")
69
75
  }
70
76
  // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
71
77
  // for those written before the switch to updates as migrations.
@@ -0,0 +1,60 @@
1
+ import { join } from 'node:path';
2
+ import * as recast from 'recast';
3
+ import { migrationsDebug } from '../../utils.js';
4
+ import { parseAsTypescript, findObjectProperty, printAST } from '../../utils.ast.js';
5
+
6
+ const { builders } = recast.types;
7
+ const BUNDLER_CONFIG_PATHS = [
8
+ join(".config", "webpack", "webpack.config.ts"),
9
+ join(".config", "rspack", "rspack.config.ts")
10
+ ];
11
+ function migrate(context) {
12
+ for (const configPath of BUNDLER_CONFIG_PATHS) {
13
+ migrateBundlerConfig(context, configPath);
14
+ }
15
+ return context;
16
+ }
17
+ function migrateBundlerConfig(context, configPath) {
18
+ if (!context.doesFileExist(configPath)) {
19
+ return;
20
+ }
21
+ const source = context.getFile(configPath);
22
+ if (!source) {
23
+ return;
24
+ }
25
+ if (source.includes("extractComments")) {
26
+ return;
27
+ }
28
+ const parsed = parseAsTypescript(source);
29
+ if (!parsed.success) {
30
+ migrationsDebug(`Failed to parse ${configPath}. Error: ${parsed.error.message}`);
31
+ return;
32
+ }
33
+ let hasChanges = false;
34
+ recast.visit(parsed.ast, {
35
+ visitNewExpression(path) {
36
+ const { node } = path;
37
+ if (node.callee.type === "Identifier" && node.callee.name === "TerserPlugin" && node.arguments.length > 0) {
38
+ const optionsArg = node.arguments[0];
39
+ if (optionsArg.type === "ObjectExpression" && !findObjectProperty(optionsArg, "extractComments")) {
40
+ const extractCommentsProperty = builders.property(
41
+ "init",
42
+ builders.identifier("extractComments"),
43
+ builders.objectExpression([
44
+ builders.property("init", builders.identifier("banner"), builders.literal(false)),
45
+ builders.property("init", builders.identifier("filename"), builders.literal("LICENSE.txt"))
46
+ ])
47
+ );
48
+ optionsArg.properties.unshift(extractCommentsProperty);
49
+ hasChanges = true;
50
+ }
51
+ }
52
+ return this.traverse(path);
53
+ }
54
+ });
55
+ if (hasChanges) {
56
+ context.updateFile(configPath, printAST(parsed.ast));
57
+ }
58
+ }
59
+
60
+ export { migrate as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/create-plugin",
3
- "version": "7.8.1",
3
+ "version": "7.9.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "directory": "packages/create-plugin",
@@ -51,7 +51,7 @@
51
51
  "@types/semver": "7.7.1",
52
52
  "@types/tmp": "0.2.6",
53
53
  "@types/which": "3.0.4",
54
- "tmp": "0.2.6"
54
+ "tmp": "0.2.7"
55
55
  },
56
56
  "engines": {
57
57
  "node": ">=20"
@@ -78,6 +78,12 @@ export default [
78
78
  'Security: replace insecure inline `npx --yes @grafana/sign-plugin@latest` sign script with a locked @grafana/sign-plugin devDependency to prevent arbitrary code execution from a compromised @latest publish.',
79
79
  scriptPath: import.meta.resolve('./scripts/011-secure-sign-script.js'),
80
80
  },
81
+ {
82
+ name: '012-terser-license-file',
83
+ version: '7.8.2',
84
+ description: 'Emit a single LICENSE.txt file for all licenses in bundled code.',
85
+ scriptPath: import.meta.resolve('./scripts/012-terser-license-file.js'),
86
+ },
81
87
  // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
82
88
  // for those written before the switch to updates as migrations.
83
89
  ] satisfies Migration[];
@@ -0,0 +1,170 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import migrate from './012-terser-license-file.js';
3
+ import { Context } from '../../context.js';
4
+
5
+ const WEBPACK_CONFIG_PATH = '.config/webpack/webpack.config.ts';
6
+ const RSPACK_CONFIG_PATH = '.config/rspack/rspack.config.ts';
7
+
8
+ const BUNDLER_CONFIG = `/*
9
+ * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY \`@grafana/create-plugin\`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
10
+ *
11
+ * In order to extend the configuration follow the steps in
12
+ * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-webpack-config
13
+ */
14
+
15
+ import TerserPlugin from 'terser-webpack-plugin';
16
+ import { type Configuration } from 'webpack';
17
+
18
+ const config = async (env: Record<string, unknown>): Promise<Configuration> => ({
19
+ optimization: {
20
+ minimize: Boolean(env.production),
21
+ minimizer: [
22
+ new TerserPlugin({
23
+ terserOptions: {
24
+ format: {
25
+ comments: (_, { type, value }) => type === 'comment2' && value.trim().startsWith('[create-plugin]'),
26
+ },
27
+ compress: {
28
+ drop_console: ['log', 'info'],
29
+ },
30
+ },
31
+ }),
32
+ ],
33
+ },
34
+ });
35
+
36
+ export default config;
37
+ `;
38
+
39
+ const MIGRATED_BUNDLER_CONFIG = `/*
40
+ * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY \`@grafana/create-plugin\`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
41
+ *
42
+ * In order to extend the configuration follow the steps in
43
+ * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-webpack-config
44
+ */
45
+
46
+ import TerserPlugin from 'terser-webpack-plugin';
47
+ import { type Configuration } from 'webpack';
48
+
49
+ const config = async (env: Record<string, unknown>): Promise<Configuration> => ({
50
+ optimization: {
51
+ minimize: Boolean(env.production),
52
+ minimizer: [
53
+ new TerserPlugin({
54
+ extractComments: {
55
+ banner: false,
56
+ filename: 'LICENSE.txt',
57
+ },
58
+
59
+ terserOptions: {
60
+ format: {
61
+ comments: (_, { type, value }) => type === 'comment2' && value.trim().startsWith('[create-plugin]'),
62
+ },
63
+ compress: {
64
+ drop_console: ['log', 'info'],
65
+ },
66
+ },
67
+ }),
68
+ ],
69
+ },
70
+ });
71
+
72
+ export default config;
73
+ `;
74
+
75
+ const NO_TERSER_CONFIG = `import { type Configuration } from 'webpack';
76
+
77
+ const config = async (): Promise<Configuration> => ({
78
+ optimization: {
79
+ minimize: true,
80
+ },
81
+ });
82
+
83
+ export default config;
84
+ `;
85
+
86
+ describe('012-terser-license-file', () => {
87
+ it('should add extractComments before terserOptions in the webpack config', () => {
88
+ const context = new Context('/virtual');
89
+ context.addFile(WEBPACK_CONFIG_PATH, BUNDLER_CONFIG);
90
+
91
+ const result = migrate(context);
92
+ const updated = result.getFile(WEBPACK_CONFIG_PATH) ?? '';
93
+
94
+ expect(updated).toContain('extractComments');
95
+ expect(updated).toContain('banner: false');
96
+ expect(updated).toContain("filename: 'LICENSE.txt'");
97
+ expect(updated.indexOf('extractComments')).toBeLessThan(updated.indexOf('terserOptions'));
98
+ });
99
+
100
+ it('should add extractComments before terserOptions in the rspack config', () => {
101
+ const context = new Context('/virtual');
102
+ context.addFile(RSPACK_CONFIG_PATH, BUNDLER_CONFIG);
103
+
104
+ const result = migrate(context);
105
+ const updated = result.getFile(RSPACK_CONFIG_PATH) ?? '';
106
+
107
+ expect(updated).toContain('extractComments');
108
+ expect(updated).toContain('banner: false');
109
+ expect(updated).toContain("filename: 'LICENSE.txt'");
110
+ expect(updated.indexOf('extractComments')).toBeLessThan(updated.indexOf('terserOptions'));
111
+ });
112
+
113
+ it('should migrate both bundler configs when both exist', () => {
114
+ const context = new Context('/virtual');
115
+ context.addFile(WEBPACK_CONFIG_PATH, BUNDLER_CONFIG);
116
+ context.addFile(RSPACK_CONFIG_PATH, BUNDLER_CONFIG);
117
+
118
+ const result = migrate(context);
119
+
120
+ expect(result.getFile(WEBPACK_CONFIG_PATH)).toContain('extractComments');
121
+ expect(result.getFile(RSPACK_CONFIG_PATH)).toContain('extractComments');
122
+ });
123
+
124
+ it('should print the extractComments block with matching formatting', () => {
125
+ const context = new Context('/virtual');
126
+ context.addFile(WEBPACK_CONFIG_PATH, BUNDLER_CONFIG);
127
+
128
+ const result = migrate(context);
129
+
130
+ expect(result.getFile(WEBPACK_CONFIG_PATH)).toBe(MIGRATED_BUNDLER_CONFIG);
131
+ });
132
+
133
+ it('should be idempotent', async () => {
134
+ const context = new Context('/virtual');
135
+ context.addFile(WEBPACK_CONFIG_PATH, BUNDLER_CONFIG);
136
+ context.addFile(RSPACK_CONFIG_PATH, BUNDLER_CONFIG);
137
+
138
+ await expect(migrate).toBeIdempotent(context);
139
+ });
140
+
141
+ it('should not modify a config with a pre-existing extractComments setting', () => {
142
+ const customizedConfig = BUNDLER_CONFIG.replace(
143
+ 'terserOptions: {',
144
+ 'extractComments: false,\n terserOptions: {'
145
+ );
146
+ const context = new Context('/virtual');
147
+ context.addFile(WEBPACK_CONFIG_PATH, customizedConfig);
148
+
149
+ const result = migrate(context);
150
+
151
+ expect(result.getFile(WEBPACK_CONFIG_PATH)).toBe(customizedConfig);
152
+ });
153
+
154
+ it('should do nothing when no bundler config exists', () => {
155
+ const context = new Context('/virtual');
156
+
157
+ const result = migrate(context);
158
+
159
+ expect(result.hasChanges()).toBe(false);
160
+ });
161
+
162
+ it('should not modify a config without a TerserPlugin', () => {
163
+ const context = new Context('/virtual');
164
+ context.addFile(WEBPACK_CONFIG_PATH, NO_TERSER_CONFIG);
165
+
166
+ const result = migrate(context);
167
+
168
+ expect(result.getFile(WEBPACK_CONFIG_PATH)).toBe(NO_TERSER_CONFIG);
169
+ });
170
+ });
@@ -0,0 +1,75 @@
1
+ import { join } from 'node:path';
2
+ import * as recast from 'recast';
3
+ import type { Context } from '../../context.js';
4
+ import { migrationsDebug } from '../../utils.js';
5
+ import { findObjectProperty, parseAsTypescript, printAST } from '../../utils.ast.js';
6
+
7
+ const { builders } = recast.types;
8
+
9
+ const BUNDLER_CONFIG_PATHS = [
10
+ join('.config', 'webpack', 'webpack.config.ts'),
11
+ join('.config', 'rspack', 'rspack.config.ts'),
12
+ ];
13
+
14
+ export default function migrate(context: Context): Context {
15
+ for (const configPath of BUNDLER_CONFIG_PATHS) {
16
+ migrateBundlerConfig(context, configPath);
17
+ }
18
+
19
+ return context;
20
+ }
21
+
22
+ function migrateBundlerConfig(context: Context, configPath: string): void {
23
+ if (!context.doesFileExist(configPath)) {
24
+ return;
25
+ }
26
+
27
+ const source = context.getFile(configPath);
28
+ if (!source) {
29
+ return;
30
+ }
31
+
32
+ // Any existing mention of extractComments (including user-supplied values
33
+ // like `extractComments: false`) means we leave the file untouched.
34
+ if (source.includes('extractComments')) {
35
+ return;
36
+ }
37
+
38
+ const parsed = parseAsTypescript(source);
39
+ if (!parsed.success) {
40
+ migrationsDebug(`Failed to parse ${configPath}. Error: ${parsed.error.message}`);
41
+ return;
42
+ }
43
+
44
+ let hasChanges = false;
45
+
46
+ recast.visit(parsed.ast, {
47
+ visitNewExpression(path) {
48
+ const { node } = path;
49
+
50
+ if (node.callee.type === 'Identifier' && node.callee.name === 'TerserPlugin' && node.arguments.length > 0) {
51
+ const optionsArg = node.arguments[0];
52
+
53
+ if (optionsArg.type === 'ObjectExpression' && !findObjectProperty(optionsArg, 'extractComments')) {
54
+ const extractCommentsProperty = builders.property(
55
+ 'init',
56
+ builders.identifier('extractComments'),
57
+ builders.objectExpression([
58
+ builders.property('init', builders.identifier('banner'), builders.literal(false)),
59
+ builders.property('init', builders.identifier('filename'), builders.literal('LICENSE.txt')),
60
+ ])
61
+ );
62
+
63
+ optionsArg.properties.unshift(extractCommentsProperty);
64
+ hasChanges = true;
65
+ }
66
+ }
67
+
68
+ return this.traverse(path);
69
+ },
70
+ });
71
+
72
+ if (hasChanges) {
73
+ context.updateFile(configPath, printAST(parsed.ast));
74
+ }
75
+ }
@@ -7,7 +7,7 @@ services:
7
7
  context: .
8
8
  args:
9
9
  grafana_image: ${GRAFANA_IMAGE:-{{~grafanaImage~}} }
10
- grafana_version: ${GRAFANA_VERSION:-13.0.2}
10
+ grafana_version: ${GRAFANA_VERSION:-13.1.0}
11
11
  development: ${DEVELOPMENT:-false}
12
12
  anonymous_auth_enabled: ${ANONYMOUS_AUTH_ENABLED:-true}
13
13
  ports:
@@ -118,6 +118,11 @@ const config = async (env): Promise<Configuration> => {
118
118
  minimize: Boolean(env.production),
119
119
  minimizer: [
120
120
  new TerserPlugin({
121
+ // Emit a single LICENSE.txt file for all comments.
122
+ extractComments: {
123
+ banner: false,
124
+ filename: 'LICENSE.txt',
125
+ },
121
126
  terserOptions: {
122
127
  format: {
123
128
  comments: (_, { type, value }) => type === 'comment2' && value.trim().startsWith('[create-plugin]'),
@@ -130,6 +130,11 @@ const config = async (env: Env): Promise<Configuration> => {
130
130
  minimize: Boolean(env.production),
131
131
  minimizer: [
132
132
  new TerserPlugin({
133
+ // Emit a single LICENSE.txt file for all comments.
134
+ extractComments: {
135
+ banner: false,
136
+ filename: 'LICENSE.txt',
137
+ },
133
138
  terserOptions: {
134
139
  format: {
135
140
  comments: (_, { type, value }) => type === 'comment2' && value.trim().startsWith('[create-plugin]'),
@@ -74,11 +74,11 @@
74
74
  },
75
75
  "dependencies": {
76
76
  "@emotion/css": "11.10.6",
77
- "@grafana/data": "13.0.2",
78
- "@grafana/i18n": "13.0.2",
79
- "@grafana/runtime": "13.0.2",
80
- "@grafana/ui": "13.0.2",
81
- "@grafana/schema": "13.0.2",{{#if_eq pluginType "scenesapp" }}
77
+ "@grafana/data": "13.1.0",
78
+ "@grafana/i18n": "13.1.0",
79
+ "@grafana/runtime": "13.1.0",
80
+ "@grafana/ui": "13.1.0",
81
+ "@grafana/schema": "13.1.0",{{#if_eq pluginType "scenesapp" }}
82
82
  "@grafana/scenes": "{{ scenesVersion }}",{{/if_eq}}
83
83
  "react": "^18.3.0",
84
84
  "react-dom": "^18.3.0"{{#if isAppType}},