@grafana/create-plugin 6.1.1 → 6.2.0-canary.2233.18644853669.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ import { getAvailableAdditions, getAdditionByName, runAddition } from '../additions/manager.js';
2
+ import { isGitDirectory, isGitDirectoryClean } from '../utils/utils.git.js';
3
+ import { isPluginDirectory } from '../utils/utils.plugin.js';
4
+ import { output } from '../utils/utils.console.js';
5
+ import { promptI18nOptions } from './add/prompts.js';
6
+
7
+ const add = async (argv) => {
8
+ const subCommand = argv._[1];
9
+ if (!subCommand) {
10
+ const availableAdditions = getAvailableAdditions();
11
+ const additionsList = Object.values(availableAdditions).map(
12
+ (addition2) => `${addition2.name} - ${addition2.description}`
13
+ );
14
+ output.error({
15
+ title: "No addition specified",
16
+ body: [
17
+ "Usage: npx @grafana/create-plugin add <addition-name>",
18
+ "",
19
+ "Available additions:",
20
+ ...output.bulletList(additionsList)
21
+ ]
22
+ });
23
+ process.exit(1);
24
+ }
25
+ await performPreAddChecks(argv);
26
+ const addition = getAdditionByName(subCommand);
27
+ if (!addition) {
28
+ const availableAdditions = getAvailableAdditions();
29
+ const additionsList = Object.values(availableAdditions).map((addition2) => addition2.name);
30
+ output.error({
31
+ title: `Unknown addition: ${subCommand}`,
32
+ body: ["Available additions:", ...output.bulletList(additionsList)]
33
+ });
34
+ process.exit(1);
35
+ }
36
+ try {
37
+ let options = {};
38
+ switch (addition.name) {
39
+ case "i18n":
40
+ options = await promptI18nOptions();
41
+ break;
42
+ default:
43
+ break;
44
+ }
45
+ const commitChanges = argv.commit;
46
+ await runAddition(addition, options, { commitChanges });
47
+ } catch (error) {
48
+ if (error instanceof Error) {
49
+ output.error({
50
+ title: "Addition failed",
51
+ body: [error.message]
52
+ });
53
+ }
54
+ process.exit(1);
55
+ }
56
+ };
57
+ async function performPreAddChecks(argv) {
58
+ if (!await isGitDirectory() && !argv.force) {
59
+ output.error({
60
+ title: "You are not inside a git directory",
61
+ body: [
62
+ `In order to proceed please run ${output.formatCode("git init")} in the root of your project and commit your changes.`,
63
+ `(This check is necessary to make sure that the changes are easy to revert and don't interfere with any changes you currently have.`,
64
+ `In case you want to proceed as is please use the ${output.formatCode("--force")} flag.)`
65
+ ]
66
+ });
67
+ process.exit(1);
68
+ }
69
+ if (!await isGitDirectoryClean() && !argv.force) {
70
+ output.error({
71
+ title: "Please clean your repository working tree before adding features.",
72
+ body: [
73
+ "Commit your changes or stash them.",
74
+ `(This check is necessary to make sure that the changes are easy to revert and don't mess with any changes you currently have.`,
75
+ `In case you want to proceed as is please use the ${output.formatCode("--force")} flag.)`
76
+ ]
77
+ });
78
+ process.exit(1);
79
+ }
80
+ if (!isPluginDirectory() && !argv.force) {
81
+ output.error({
82
+ title: "Are you inside a plugin directory?",
83
+ body: [
84
+ `We couldn't find a "src/plugin.json" file under your current directory.`,
85
+ `(Please make sure to run this command from the root of your plugin folder. In case you want to proceed as is please use the ${output.formatCode("--force")} flag.)`
86
+ ]
87
+ });
88
+ process.exit(1);
89
+ }
90
+ }
91
+
92
+ export { add };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/create-plugin",
3
- "version": "6.1.1",
3
+ "version": "6.2.0-canary.2233.18644853669.0",
4
4
  "repository": {
5
5
  "directory": "packages/create-plugin",
6
6
  "url": "https://github.com/grafana/plugin-tools"
@@ -61,5 +61,5 @@
61
61
  "engines": {
62
62
  "node": ">=20"
63
63
  },
64
- "gitHead": "6b891abe4df5c9c3c1e191b5286298182efa70a0"
64
+ "gitHead": "4eb067f4f28b42ef18426827a1443073a97c3efd"
65
65
  }
@@ -0,0 +1,19 @@
1
+ export type AdditionMeta = {
2
+ name: string;
3
+ description: string;
4
+ scriptPath: string;
5
+ };
6
+
7
+ type Additions = {
8
+ additions: Record<string, AdditionMeta>;
9
+ };
10
+
11
+ export default {
12
+ additions: {
13
+ i18n: {
14
+ name: 'i18n',
15
+ description: 'Add internationalization (i18n) support to your plugin',
16
+ scriptPath: './scripts/add-i18n.js',
17
+ },
18
+ },
19
+ } as Additions;
@@ -0,0 +1,77 @@
1
+ import { additionsDebug, flushChanges, formatFiles, installNPMDependencies, printChanges } from './utils.js';
2
+ import defaultAdditions, { AdditionMeta } from './additions.js';
3
+
4
+ import { Context } from '../migrations/context.js';
5
+ import { gitCommitNoVerify } from '../utils/utils.git.js';
6
+ import { output } from '../utils/utils.console.js';
7
+
8
+ export type AdditionFn = (context: Context, options?: AdditionOptions) => Context | Promise<Context>;
9
+
10
+ export type AdditionOptions = Record<string, any>;
11
+
12
+ type RunAdditionOptions = {
13
+ commitChanges?: boolean;
14
+ };
15
+
16
+ export function getAvailableAdditions(
17
+ additions: Record<string, AdditionMeta> = defaultAdditions.additions
18
+ ): Record<string, AdditionMeta> {
19
+ return additions;
20
+ }
21
+
22
+ export function getAdditionByName(
23
+ name: string,
24
+ additions: Record<string, AdditionMeta> = defaultAdditions.additions
25
+ ): AdditionMeta | undefined {
26
+ return additions[name];
27
+ }
28
+
29
+ export async function runAddition(
30
+ addition: AdditionMeta,
31
+ additionOptions: AdditionOptions = {},
32
+ runOptions: RunAdditionOptions = {}
33
+ ): Promise<void> {
34
+ const basePath = process.cwd();
35
+
36
+ output.log({
37
+ title: `Running addition: ${addition.name}`,
38
+ body: [addition.description],
39
+ });
40
+
41
+ try {
42
+ const context = new Context(basePath);
43
+ const updatedContext = await executeAddition(addition, context, additionOptions);
44
+ const shouldCommit = runOptions.commitChanges && updatedContext.hasChanges();
45
+
46
+ additionsDebug(`context for "${addition.name} (${addition.scriptPath})":`);
47
+ additionsDebug('%O', updatedContext.listChanges());
48
+
49
+ await formatFiles(updatedContext);
50
+ flushChanges(updatedContext);
51
+ printChanges(updatedContext, addition.name, addition);
52
+
53
+ installNPMDependencies(updatedContext);
54
+
55
+ if (shouldCommit) {
56
+ await gitCommitNoVerify(`chore: add ${addition.name} support via create-plugin`);
57
+ }
58
+
59
+ output.success({
60
+ title: `Successfully added ${addition.name} to your plugin.`,
61
+ });
62
+ } catch (error) {
63
+ if (error instanceof Error) {
64
+ throw new Error(`Error running addition "${addition.name} (${addition.scriptPath})": ${error.message}`);
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ export async function executeAddition(
71
+ addition: AdditionMeta,
72
+ context: Context,
73
+ options: AdditionOptions = {}
74
+ ): Promise<Context> {
75
+ const module: { default: AdditionFn } = await import(addition.scriptPath);
76
+ return module.default(context, options);
77
+ }
@@ -0,0 +1,347 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { Context } from '../../migrations/context.js';
4
+ import migrate from './add-i18n.js';
5
+
6
+ describe('add-i18n', () => {
7
+ it('should be idempotent', async () => {
8
+ const context = new Context('/virtual');
9
+
10
+ // Set up a minimal plugin structure
11
+ context.addFile(
12
+ 'src/plugin.json',
13
+ JSON.stringify({
14
+ id: 'test-plugin',
15
+ type: 'panel',
16
+ name: 'Test Plugin',
17
+ dependencies: {
18
+ grafanaDependency: '>=11.0.0',
19
+ },
20
+ })
21
+ );
22
+ context.addFile('docker-compose.yaml', 'services:\n grafana:\n environment:\n FOO: bar');
23
+ context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {} }));
24
+ context.addFile(
25
+ 'eslint.config.mjs',
26
+ 'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
27
+ );
28
+ context.addFile(
29
+ 'src/module.ts',
30
+ 'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
31
+ );
32
+
33
+ const migrateWithOptions = (ctx: Context) => migrate(ctx, { locales: ['en-US'] });
34
+ await expect(migrateWithOptions).toBeIdempotent(context);
35
+ });
36
+
37
+ it('should add i18n support with a single locale (backward compatibility for Grafana < 12.1.0)', () => {
38
+ const context = new Context('/virtual');
39
+
40
+ // Set up a minimal plugin structure with Grafana 11.0.0 (needs backward compatibility)
41
+ context.addFile(
42
+ 'src/plugin.json',
43
+ JSON.stringify({
44
+ id: 'test-plugin',
45
+ type: 'panel',
46
+ name: 'Test Plugin',
47
+ dependencies: {
48
+ grafanaDependency: '>=11.0.0',
49
+ },
50
+ })
51
+ );
52
+ context.addFile(
53
+ 'docker-compose.yaml',
54
+ `services:
55
+ grafana:
56
+ environment:
57
+ FOO: bar`
58
+ );
59
+ context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {}, scripts: {} }));
60
+ context.addFile(
61
+ 'eslint.config.mjs',
62
+ 'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
63
+ );
64
+ context.addFile(
65
+ 'src/module.ts',
66
+ 'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
67
+ );
68
+
69
+ const result = migrate(context, { locales: ['en-US'] });
70
+
71
+ // Check plugin.json was updated
72
+ const pluginJson = JSON.parse(result.getFile('src/plugin.json') || '{}');
73
+ expect(pluginJson.languages).toEqual(['en-US']);
74
+ // Should stay at 11.0.0 for backward compatibility
75
+ expect(pluginJson.dependencies.grafanaDependency).toBe('>=11.0.0');
76
+
77
+ // Check locale file was created with example translations
78
+ expect(result.doesFileExist('src/locales/en-US/test-plugin.json')).toBe(true);
79
+ const localeContent = result.getFile('src/locales/en-US/test-plugin.json');
80
+ const localeData = JSON.parse(localeContent || '{}');
81
+ expect(localeData).toHaveProperty('components');
82
+ expect(localeData).toHaveProperty('config');
83
+
84
+ // Check package.json was updated with dependencies
85
+ const packageJson = JSON.parse(result.getFile('package.json') || '{}');
86
+ expect(packageJson.dependencies['@grafana/i18n']).toBe('12.2.2');
87
+ expect(packageJson.dependencies['semver']).toBe('^7.6.0');
88
+ expect(packageJson.devDependencies['@types/semver']).toBe('^7.5.0');
89
+ expect(packageJson.devDependencies['i18next-cli']).toBeDefined();
90
+ expect(packageJson.scripts['i18n-extract']).toBe('i18next-cli extract --sync-primary');
91
+
92
+ // Check docker-compose.yaml was NOT updated (backward compat doesn't add feature toggle)
93
+ const dockerCompose = result.getFile('docker-compose.yaml');
94
+ expect(dockerCompose).not.toContain('localizationForPlugins');
95
+
96
+ // Check module.ts was updated with backward compatibility code
97
+ const moduleTs = result.getFile('src/module.ts');
98
+ expect(moduleTs).toContain('initPluginTranslations');
99
+ expect(moduleTs).toContain('semver');
100
+ expect(moduleTs).toContain('loadResources');
101
+
102
+ // Check loadResources.ts was created for backward compatibility
103
+ expect(result.doesFileExist('src/loadResources.ts')).toBe(true);
104
+ const loadResources = result.getFile('src/loadResources.ts');
105
+ expect(loadResources).toContain('ResourceLoader');
106
+
107
+ // Check i18next.config.ts was created
108
+ expect(result.doesFileExist('i18next.config.ts')).toBe(true);
109
+ const i18nextConfig = result.getFile('i18next.config.ts');
110
+ expect(i18nextConfig).toContain('defineConfig');
111
+ expect(i18nextConfig).toContain('pluginJson.id');
112
+ });
113
+
114
+ it('should add i18n support with multiple locales', () => {
115
+ const context = new Context('/virtual');
116
+
117
+ context.addFile(
118
+ 'src/plugin.json',
119
+ JSON.stringify({
120
+ id: 'test-plugin',
121
+ type: 'panel',
122
+ name: 'Test Plugin',
123
+ dependencies: {
124
+ grafanaDependency: '>=11.0.0',
125
+ },
126
+ })
127
+ );
128
+ context.addFile(
129
+ 'docker-compose.yaml',
130
+ `services:
131
+ grafana:
132
+ environment:
133
+ FOO: bar`
134
+ );
135
+ context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {}, scripts: {} }));
136
+ context.addFile(
137
+ 'eslint.config.mjs',
138
+ 'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
139
+ );
140
+ context.addFile(
141
+ 'src/module.ts',
142
+ 'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
143
+ );
144
+
145
+ const result = migrate(context, { locales: ['en-US', 'es-ES', 'sv-SE'] });
146
+
147
+ // Check plugin.json has all locales
148
+ const pluginJson = JSON.parse(result.getFile('src/plugin.json') || '{}');
149
+ expect(pluginJson.languages).toEqual(['en-US', 'es-ES', 'sv-SE']);
150
+
151
+ // Check all locale files were created
152
+ expect(result.doesFileExist('src/locales/en-US/test-plugin.json')).toBe(true);
153
+ expect(result.doesFileExist('src/locales/es-ES/test-plugin.json')).toBe(true);
154
+ expect(result.doesFileExist('src/locales/sv-SE/test-plugin.json')).toBe(true);
155
+ });
156
+
157
+ it('should skip if i18n is already configured', () => {
158
+ const context = new Context('/virtual');
159
+
160
+ // Set up a plugin with i18n already configured
161
+ context.addFile(
162
+ 'src/plugin.json',
163
+ JSON.stringify({
164
+ id: 'test-plugin',
165
+ type: 'panel',
166
+ name: 'Test Plugin',
167
+ languages: ['en-US'], // Already configured
168
+ dependencies: {
169
+ grafanaDependency: '>=12.1.0',
170
+ },
171
+ })
172
+ );
173
+ context.addFile(
174
+ 'docker-compose.yaml',
175
+ `services:
176
+ grafana:
177
+ environment:
178
+ FOO: bar`
179
+ );
180
+ context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {}, scripts: {} }));
181
+ context.addFile(
182
+ 'eslint.config.mjs',
183
+ 'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
184
+ );
185
+ context.addFile(
186
+ 'src/module.ts',
187
+ 'import { PanelPlugin } from "@grafana/data";\nimport { i18n } from "@grafana/i18n";\nexport const plugin = new PanelPlugin();'
188
+ );
189
+
190
+ // Flush the context to simulate these files existing on "disk"
191
+ const initialChanges = Object.keys(context.listChanges()).length;
192
+
193
+ const result = migrate(context, { locales: ['es-ES'] });
194
+
195
+ // Should not add any NEW changes beyond the initial setup
196
+ const finalChanges = Object.keys(result.listChanges()).length;
197
+ expect(finalChanges).toBe(initialChanges);
198
+ });
199
+
200
+ it('should handle existing feature toggles in docker-compose.yaml (Grafana >= 12.1.0)', () => {
201
+ const context = new Context('/virtual');
202
+
203
+ context.addFile(
204
+ 'src/plugin.json',
205
+ JSON.stringify({
206
+ id: 'test-plugin',
207
+ type: 'panel',
208
+ name: 'Test Plugin',
209
+ dependencies: {
210
+ grafanaDependency: '>=12.1.0',
211
+ },
212
+ })
213
+ );
214
+ context.addFile(
215
+ 'docker-compose.yaml',
216
+ `services:
217
+ grafana:
218
+ environment:
219
+ GF_FEATURE_TOGGLES_ENABLE: someOtherFeature`
220
+ );
221
+ context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {}, scripts: {} }));
222
+ context.addFile(
223
+ 'eslint.config.mjs',
224
+ 'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
225
+ );
226
+ context.addFile(
227
+ 'src/module.ts',
228
+ 'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
229
+ );
230
+
231
+ const result = migrate(context, { locales: ['en-US'] });
232
+
233
+ const dockerCompose = result.getFile('docker-compose.yaml');
234
+ expect(dockerCompose).toContain('someOtherFeature,localizationForPlugins');
235
+ });
236
+
237
+ it('should work with module.tsx instead of module.ts', () => {
238
+ const context = new Context('/virtual');
239
+
240
+ context.addFile(
241
+ 'src/plugin.json',
242
+ JSON.stringify({
243
+ id: 'test-plugin',
244
+ type: 'panel',
245
+ name: 'Test Plugin',
246
+ dependencies: {
247
+ grafanaDependency: '>=11.0.0',
248
+ },
249
+ })
250
+ );
251
+ context.addFile(
252
+ 'docker-compose.yaml',
253
+ `services:
254
+ grafana:
255
+ environment:
256
+ FOO: bar`
257
+ );
258
+ context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {}, scripts: {} }));
259
+ context.addFile(
260
+ 'eslint.config.mjs',
261
+ 'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
262
+ );
263
+ context.addFile(
264
+ 'src/module.tsx',
265
+ 'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
266
+ );
267
+
268
+ const result = migrate(context, { locales: ['en-US'] });
269
+
270
+ const moduleTsx = result.getFile('src/module.tsx');
271
+ expect(moduleTsx).toContain('@grafana/i18n');
272
+ });
273
+
274
+ it('should not update grafanaDependency if it is already >= 12.1.0', () => {
275
+ const context = new Context('/virtual');
276
+
277
+ context.addFile(
278
+ 'src/plugin.json',
279
+ JSON.stringify({
280
+ id: 'test-plugin',
281
+ type: 'panel',
282
+ name: 'Test Plugin',
283
+ dependencies: {
284
+ grafanaDependency: '>=13.0.0',
285
+ },
286
+ })
287
+ );
288
+ context.addFile(
289
+ 'docker-compose.yaml',
290
+ `services:
291
+ grafana:
292
+ environment:
293
+ FOO: bar`
294
+ );
295
+ context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {}, scripts: {} }));
296
+ context.addFile(
297
+ 'eslint.config.mjs',
298
+ 'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
299
+ );
300
+ context.addFile(
301
+ 'src/module.ts',
302
+ 'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
303
+ );
304
+
305
+ const result = migrate(context, { locales: ['en-US'] });
306
+
307
+ const pluginJson = JSON.parse(result.getFile('src/plugin.json') || '{}');
308
+ expect(pluginJson.dependencies.grafanaDependency).toBe('>=13.0.0');
309
+ });
310
+
311
+ it('should handle plugins without existing scripts in package.json', () => {
312
+ const context = new Context('/virtual');
313
+
314
+ context.addFile(
315
+ 'src/plugin.json',
316
+ JSON.stringify({
317
+ id: 'test-plugin',
318
+ type: 'panel',
319
+ name: 'Test Plugin',
320
+ dependencies: {
321
+ grafanaDependency: '>=11.0.0',
322
+ },
323
+ })
324
+ );
325
+ context.addFile(
326
+ 'docker-compose.yaml',
327
+ `services:
328
+ grafana:
329
+ environment:
330
+ FOO: bar`
331
+ );
332
+ context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {} })); // No scripts field
333
+ context.addFile(
334
+ 'eslint.config.mjs',
335
+ 'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
336
+ );
337
+ context.addFile(
338
+ 'src/module.ts',
339
+ 'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
340
+ );
341
+
342
+ const result = migrate(context, { locales: ['en-US'] });
343
+
344
+ const packageJson = JSON.parse(result.getFile('package.json') || '{}');
345
+ expect(packageJson.scripts['i18n-extract']).toBe('i18next-cli extract --sync-primary');
346
+ });
347
+ });