@grafana/create-plugin 6.2.0-canary.2233.18586197736.0 → 6.2.0-canary.2233.18936109308.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.
@@ -1,11 +1,12 @@
1
- import fs from 'node:fs';
2
- import { writeFile } from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { CURRENT_APP_VERSION } from './utils.version.js';
5
1
  import { argv, commandName } from './utils.cli.js';
2
+ import { CURRENT_APP_VERSION } from './utils.version.js';
6
3
  import { DEFAULT_FEATURE_FLAGS } from '../constants.js';
4
+ import fs from 'node:fs';
7
5
  import { output } from './utils.console.js';
8
6
  import { partitionArr } from './utils.helpers.js';
7
+ import path from 'node:path';
8
+ import { writeFile } from 'node:fs/promises';
9
+ import { EOL } from 'node:os';
9
10
 
10
11
  let hasShownConfigWarnings = false;
11
12
  function getConfig(workDir = process.cwd()) {
@@ -88,7 +89,7 @@ async function setRootConfig(configOverride = {}) {
88
89
  const rootConfig = getRootConfig();
89
90
  const rootConfigPath = path.resolve(process.cwd(), ".config/.cprc.json");
90
91
  const updatedConfig = { ...rootConfig, ...configOverride };
91
- await writeFile(rootConfigPath, JSON.stringify(updatedConfig, null, 2));
92
+ await writeFile(rootConfigPath, JSON.stringify(updatedConfig, null, 2) + EOL);
92
93
  return updatedConfig;
93
94
  }
94
95
 
@@ -1,15 +1,14 @@
1
- import { lt } from 'semver';
2
- import { glob } from 'glob';
3
- import path from 'node:path';
4
- import fs from 'node:fs';
1
+ import { DEFAULT_FEATURE_FLAGS, EXTRA_TEMPLATE_VARIABLES, PLUGIN_TYPES, TEMPLATE_PATHS, EXPORT_PATH_PREFIX } from '../constants.js';
5
2
  import { isFile, getExportFileName, filterOutCommonFiles, isFileStartingWith } from './utils.files.js';
3
+ import { getPackageManagerFromUserAgent, getPackageManagerInstallCmd, getPackageManagerWithFallback } from './utils.packageManager.js';
6
4
  import { normalizeId, renderHandlebarsTemplate } from './utils.handlebars.js';
7
- import { getPluginJson } from './utils.plugin.js';
5
+ import { CURRENT_APP_VERSION } from './utils.version.js';
8
6
  import { debug } from './utils.cli.js';
9
- import { DEFAULT_FEATURE_FLAGS, EXTRA_TEMPLATE_VARIABLES, PLUGIN_TYPES, TEMPLATE_PATHS, EXPORT_PATH_PREFIX } from '../constants.js';
10
- import { getPackageManagerFromUserAgent, getPackageManagerInstallCmd, getPackageManagerWithFallback } from './utils.packageManager.js';
11
- import { getGrafanaRuntimeVersion, CURRENT_APP_VERSION } from './utils.version.js';
7
+ import fs from 'node:fs';
12
8
  import { getConfig } from './utils.config.js';
9
+ import { getPluginJson } from './utils.plugin.js';
10
+ import { glob } from 'glob';
11
+ import path from 'node:path';
13
12
 
14
13
  const templatesDebugger = debug.extend("templates");
15
14
  function getTemplateFiles(pluginType, filter) {
@@ -57,9 +56,6 @@ function renderTemplateFromFile(templateFile, data) {
57
56
  function getTemplateData(cliArgs) {
58
57
  const { features } = getConfig();
59
58
  const currentVersion = CURRENT_APP_VERSION;
60
- const grafanaVersion = getGrafanaRuntimeVersion();
61
- const usePlaywright = features.usePlaywright === true || isFile(path.join(process.cwd(), "playwright.config.ts"));
62
- const useCypress = !usePlaywright && lt(grafanaVersion, "11.0.0") && fs.existsSync(path.join(process.cwd(), "cypress"));
63
59
  const bundleGrafanaUI = features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI;
64
60
  const getReactRouterVersion = () => features.useReactRouterV6 ? "6.22.0" : "5.2.0";
65
61
  const isAppType = (pluginType) => pluginType === PLUGIN_TYPES.app || pluginType === PLUGIN_TYPES.scenes;
@@ -86,8 +82,6 @@ function getTemplateData(cliArgs) {
86
82
  useReactRouterV6: features.useReactRouterV6 ?? DEFAULT_FEATURE_FLAGS.useReactRouterV6,
87
83
  reactRouterVersion: getReactRouterVersion(),
88
84
  scenesVersion: features.useReactRouterV6 ? "^6.10.4" : "^5.41.3",
89
- usePlaywright,
90
- useCypress,
91
85
  useExperimentalRspack: Boolean(features.useExperimentalRspack),
92
86
  frontendBundler
93
87
  };
@@ -111,8 +105,6 @@ function getTemplateData(cliArgs) {
111
105
  useReactRouterV6: features.useReactRouterV6 ?? DEFAULT_FEATURE_FLAGS.useReactRouterV6,
112
106
  reactRouterVersion: getReactRouterVersion(),
113
107
  scenesVersion: features.useReactRouterV6 ? "^6.10.4" : "^5.41.3",
114
- usePlaywright,
115
- useCypress,
116
108
  pluginExecutable: pluginJson.executable,
117
109
  useExperimentalRspack: Boolean(features.useExperimentalRspack),
118
110
  frontendBundler
@@ -1,14 +1,8 @@
1
- import { readFileSync } from 'node:fs';
2
- import path from 'node:path';
1
+ import 'node:fs';
2
+ import 'node:path';
3
3
  import { getVersion } from '../libs/version/src/index.js';
4
- import { TEMPLATE_PATHS } from '../constants.js';
4
+ import '../constants.js';
5
5
 
6
6
  const CURRENT_APP_VERSION = getVersion();
7
- function getGrafanaRuntimeVersion() {
8
- const packageJsonPath = path.join(TEMPLATE_PATHS.common, "_package.json");
9
- const pkg = readFileSync(packageJsonPath, "utf8");
10
- const { version } = /\"(@grafana\/runtime)\":\s\"\^(?<version>.*)\"/.exec(pkg)?.groups ?? {};
11
- return version;
12
- }
13
7
 
14
- export { CURRENT_APP_VERSION, getGrafanaRuntimeVersion };
8
+ export { CURRENT_APP_VERSION };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/create-plugin",
3
- "version": "6.2.0-canary.2233.18586197736.0",
3
+ "version": "6.2.0-canary.2233.18936109308.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": "0e9c9708ed87727b5956b44fcdaa3bbc0d8221f8"
64
+ "gitHead": "94a6935ff89c5160c02a8031167731268431b004"
65
65
  }
@@ -4,10 +4,6 @@ export type AdditionMeta = {
4
4
  scriptPath: string;
5
5
  };
6
6
 
7
- type Additions = {
8
- additions: Record<string, AdditionMeta>;
9
- };
10
-
11
7
  export default {
12
8
  additions: {
13
9
  i18n: {
@@ -16,4 +12,4 @@ export default {
16
12
  scriptPath: './scripts/add-i18n.js',
17
13
  },
18
14
  },
19
- } as Additions;
15
+ };
@@ -26,6 +26,73 @@ export function getAdditionByName(
26
26
  return additions[name];
27
27
  }
28
28
 
29
+ export async function getAdditionFlags(addition: AdditionMeta): Promise<any[]> {
30
+ try {
31
+ const module = await import(addition.scriptPath);
32
+ return module.flags || [];
33
+ } catch (error) {
34
+ return [];
35
+ }
36
+ }
37
+
38
+ export async function parseAdditionFlags(addition: AdditionMeta, argv: any): Promise<AdditionOptions> {
39
+ try {
40
+ const module = await import(addition.scriptPath);
41
+ if (module.parseFlags && typeof module.parseFlags === 'function') {
42
+ return module.parseFlags(argv);
43
+ }
44
+ return {};
45
+ } catch (error) {
46
+ return {};
47
+ }
48
+ }
49
+
50
+ async function validateAdditionOptions(addition: AdditionMeta, options: AdditionOptions): Promise<void> {
51
+ const flags = await getAdditionFlags(addition);
52
+
53
+ if (!flags || flags.length === 0) {
54
+ return;
55
+ }
56
+
57
+ const missingFlags: string[] = [];
58
+
59
+ for (const flag of flags) {
60
+ if (flag.required) {
61
+ const value = options[flag.name];
62
+ if (value === undefined || value === null || (Array.isArray(value) && value.length === 0)) {
63
+ missingFlags.push(flag.name);
64
+ }
65
+ }
66
+ }
67
+
68
+ if (missingFlags.length > 0) {
69
+ const flagDocs = flags.filter((f) => missingFlags.includes(f.name)).map((f) => ` --${f.name}: ${f.description}`);
70
+
71
+ throw new Error(
72
+ `Missing required flag${missingFlags.length > 1 ? 's' : ''}:\n\n` +
73
+ flagDocs.join('\n') +
74
+ `\n\nExample: npx @grafana/create-plugin add ${addition.name} --${missingFlags[0]}=value`
75
+ );
76
+ }
77
+ }
78
+
79
+ export async function runAdditionByName(
80
+ additionName: string,
81
+ argv: any,
82
+ runOptions: RunAdditionOptions = {}
83
+ ): Promise<void> {
84
+ const addition = getAdditionByName(additionName);
85
+ if (!addition) {
86
+ const availableAdditions = getAvailableAdditions();
87
+ const additionsList = Object.keys(availableAdditions);
88
+ throw new Error(`Unknown addition: ${additionName}\n\nAvailable additions: ${additionsList.join(', ')}`);
89
+ }
90
+
91
+ const options = await parseAdditionFlags(addition, argv);
92
+ await validateAdditionOptions(addition, options);
93
+ await runAddition(addition, options, runOptions);
94
+ }
95
+
29
96
  export async function runAddition(
30
97
  addition: AdditionMeta,
31
98
  additionOptions: AdditionOptions = {},
@@ -34,10 +34,10 @@ describe('add-i18n', () => {
34
34
  await expect(migrateWithOptions).toBeIdempotent(context);
35
35
  });
36
36
 
37
- it('should add i18n support with a single locale', () => {
37
+ it('should add i18n support with a single locale (backward compatibility for Grafana < 12.1.0)', () => {
38
38
  const context = new Context('/virtual');
39
39
 
40
- // Set up a minimal plugin structure
40
+ // Set up a minimal plugin structure with Grafana 11.0.0 (needs backward compatibility)
41
41
  context.addFile(
42
42
  'src/plugin.json',
43
43
  JSON.stringify({
@@ -71,26 +71,38 @@ describe('add-i18n', () => {
71
71
  // Check plugin.json was updated
72
72
  const pluginJson = JSON.parse(result.getFile('src/plugin.json') || '{}');
73
73
  expect(pluginJson.languages).toEqual(['en-US']);
74
- expect(pluginJson.dependencies.grafanaDependency).toBe('>=12.1.0');
74
+ // Should stay at 11.0.0 for backward compatibility
75
+ expect(pluginJson.dependencies.grafanaDependency).toBe('>=11.0.0');
75
76
 
76
- // Check locale file was created
77
+ // Check locale file was created with example translations
77
78
  expect(result.doesFileExist('src/locales/en-US/test-plugin.json')).toBe(true);
78
79
  const localeContent = result.getFile('src/locales/en-US/test-plugin.json');
79
- expect(JSON.parse(localeContent || '{}')).toEqual({});
80
+ const localeData = JSON.parse(localeContent || '{}');
81
+ expect(localeData).toHaveProperty('components');
82
+ expect(localeData).toHaveProperty('config');
80
83
 
81
84
  // Check package.json was updated with dependencies
82
85
  const packageJson = JSON.parse(result.getFile('package.json') || '{}');
83
- expect(packageJson.dependencies['@grafana/i18n']).toBeDefined();
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');
84
89
  expect(packageJson.devDependencies['i18next-cli']).toBeDefined();
85
90
  expect(packageJson.scripts['i18n-extract']).toBe('i18next-cli extract --sync-primary');
86
91
 
87
- // Check docker-compose.yaml was updated
92
+ // Check docker-compose.yaml was NOT updated (backward compat doesn't add feature toggle)
88
93
  const dockerCompose = result.getFile('docker-compose.yaml');
89
- expect(dockerCompose).toContain('localizationForPlugins');
94
+ expect(dockerCompose).not.toContain('localizationForPlugins');
90
95
 
91
- // Check module.ts was updated
96
+ // Check module.ts was updated with backward compatibility code
92
97
  const moduleTs = result.getFile('src/module.ts');
93
- expect(moduleTs).toContain('@grafana/i18n');
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');
94
106
 
95
107
  // Check i18next.config.ts was created
96
108
  expect(result.doesFileExist('i18next.config.ts')).toBe(true);
@@ -185,7 +197,7 @@ describe('add-i18n', () => {
185
197
  expect(finalChanges).toBe(initialChanges);
186
198
  });
187
199
 
188
- it('should handle existing feature toggles in docker-compose.yaml', () => {
200
+ it('should handle existing feature toggles in docker-compose.yaml (Grafana >= 12.1.0)', () => {
189
201
  const context = new Context('/virtual');
190
202
 
191
203
  context.addFile(
@@ -195,7 +207,7 @@ describe('add-i18n', () => {
195
207
  type: 'panel',
196
208
  name: 'Test Plugin',
197
209
  dependencies: {
198
- grafanaDependency: '>=11.0.0',
210
+ grafanaDependency: '>=12.1.0',
199
211
  },
200
212
  })
201
213
  );
@@ -12,6 +12,22 @@ export type I18nOptions = {
12
12
  locales: string[];
13
13
  };
14
14
 
15
+ // Flag schema for CLI
16
+ export const flags = [
17
+ {
18
+ name: 'locales',
19
+ description: 'Comma-separated list of locales (e.g., en-US,es-ES)',
20
+ required: true,
21
+ },
22
+ ];
23
+
24
+ // Parse CLI flags to options
25
+ export function parseFlags(argv: any): I18nOptions {
26
+ return {
27
+ locales: argv.locales ? argv.locales.split(',').map((l: string) => l.trim()) : [],
28
+ };
29
+ }
30
+
15
31
  export default function migrate(context: Context, options: I18nOptions = { locales: ['en-US'] }): Context {
16
32
  const { locales } = options;
17
33
 
@@ -23,28 +39,44 @@ export default function migrate(context: Context, options: I18nOptions = { local
23
39
  return context;
24
40
  }
25
41
 
26
- // 1. Update docker-compose.yaml with feature toggle
27
- updateDockerCompose(context);
42
+ // Determine if we need backward compatibility (Grafana < 12.1.0)
43
+ const needsBackwardCompatibility = checkNeedsBackwardCompatibility(context);
44
+ additionsDebug('Needs backward compatibility:', needsBackwardCompatibility);
45
+
46
+ // 1. Update docker-compose.yaml with feature toggle (only if >= 12.1.0)
47
+ if (!needsBackwardCompatibility) {
48
+ updateDockerCompose(context);
49
+ }
28
50
 
29
51
  // 2. Update plugin.json with languages and grafanaDependency
30
- updatePluginJson(context, locales);
52
+ updatePluginJson(context, locales, needsBackwardCompatibility);
31
53
 
32
- // 3. Create locale folders and files
54
+ // 3. Create locale folders and files with example translations
33
55
  createLocaleFiles(context, locales);
34
56
 
35
57
  // 4. Add @grafana/i18n dependency
36
58
  addI18nDependency(context);
37
59
 
38
- // 5. Update eslint.config.mjs if needed
60
+ // 5. Add semver dependency for backward compatibility
61
+ if (needsBackwardCompatibility) {
62
+ addSemverDependency(context);
63
+ }
64
+
65
+ // 6. Update eslint.config.mjs if needed
39
66
  updateEslintConfig(context);
40
67
 
41
- // 6. Add i18n initialization to module file
42
- addI18nInitialization(context);
68
+ // 7. Add i18n initialization to module file
69
+ addI18nInitialization(context, needsBackwardCompatibility);
43
70
 
44
- // 7. Add i18next-cli as dev dependency and add script
71
+ // 8. Create loadResources.ts for backward compatibility
72
+ if (needsBackwardCompatibility) {
73
+ createLoadResourcesFile(context);
74
+ }
75
+
76
+ // 9. Add i18next-cli as dev dependency and add script
45
77
  addI18nextCli(context);
46
78
 
47
- // 8. Create i18next.config.ts
79
+ // 10. Create i18next.config.ts
48
80
  createI18nextConfig(context);
49
81
 
50
82
  return context;
@@ -74,6 +106,29 @@ function isI18nConfigured(context: Context): boolean {
74
106
  return false;
75
107
  }
76
108
 
109
+ function checkNeedsBackwardCompatibility(context: Context): boolean {
110
+ const pluginJsonRaw = context.getFile('src/plugin.json');
111
+ if (!pluginJsonRaw) {
112
+ return false;
113
+ }
114
+
115
+ try {
116
+ const pluginJson = JSON.parse(pluginJsonRaw);
117
+ const currentGrafanaDep = pluginJson.dependencies?.grafanaDependency || '>=11.0.0';
118
+ const minVersion = coerce('12.1.0');
119
+ const currentVersion = coerce(currentGrafanaDep.replace(/^[><=]+/, ''));
120
+
121
+ // If current version is less than 12.1.0, we need backward compatibility
122
+ if (currentVersion && minVersion && gte(currentVersion, minVersion)) {
123
+ return false; // Already >= 12.1.0, no backward compat needed
124
+ }
125
+ return true; // < 12.1.0, needs backward compat
126
+ } catch (error) {
127
+ additionsDebug('Error checking backward compatibility:', error);
128
+ return true; // Default to backward compat on error
129
+ }
130
+ }
131
+
77
132
  function updateDockerCompose(context: Context): void {
78
133
  if (!context.doesFileExist('docker-compose.yaml')) {
79
134
  additionsDebug('docker-compose.yaml not found, skipping');
@@ -123,7 +178,7 @@ function updateDockerCompose(context: Context): void {
123
178
  }
124
179
  }
125
180
 
126
- function updatePluginJson(context: Context, locales: string[]): void {
181
+ function updatePluginJson(context: Context, locales: string[], needsBackwardCompatibility: boolean): void {
127
182
  if (!context.doesFileExist('src/plugin.json')) {
128
183
  additionsDebug('src/plugin.json not found, skipping');
129
184
  return;
@@ -140,18 +195,19 @@ function updatePluginJson(context: Context, locales: string[]): void {
140
195
  // Add languages array
141
196
  pluginJson.languages = locales;
142
197
 
143
- // Ensure grafanaDependency is >= 12.1.0
198
+ // Update grafanaDependency based on backward compatibility needs
144
199
  if (!pluginJson.dependencies) {
145
200
  pluginJson.dependencies = {};
146
201
  }
147
202
 
148
203
  const currentGrafanaDep = pluginJson.dependencies.grafanaDependency || '>=11.0.0';
149
- const minVersion = coerce('12.1.0');
204
+ const targetVersion = needsBackwardCompatibility ? '11.0.0' : '12.1.0';
205
+ const minVersion = coerce(targetVersion);
150
206
  const currentVersion = coerce(currentGrafanaDep.replace(/^[><=]+/, ''));
151
207
 
152
208
  if (!currentVersion || !minVersion || !gte(currentVersion, minVersion)) {
153
- pluginJson.dependencies.grafanaDependency = '>=12.1.0';
154
- additionsDebug('Updated grafanaDependency to >=12.1.0');
209
+ pluginJson.dependencies.grafanaDependency = `>=${targetVersion}`;
210
+ additionsDebug(`Updated grafanaDependency to >=${targetVersion}`);
155
211
  }
156
212
 
157
213
  context.updateFile('src/plugin.json', JSON.stringify(pluginJson, null, 2));
@@ -172,19 +228,37 @@ function createLocaleFiles(context: Context, locales: string[]): void {
172
228
  try {
173
229
  const pluginJson = JSON.parse(pluginJsonRaw);
174
230
  const pluginId = pluginJson.id;
231
+ const pluginName = pluginJson.name || pluginId;
175
232
 
176
233
  if (!pluginId) {
177
234
  additionsDebug('No plugin ID found in plugin.json');
178
235
  return;
179
236
  }
180
237
 
238
+ // Create example translation structure
239
+ const exampleTranslations = {
240
+ components: {
241
+ exampleComponent: {
242
+ title: `${pluginName} component title`,
243
+ description: 'Example description',
244
+ },
245
+ },
246
+ config: {
247
+ title: `${pluginName} configuration`,
248
+ apiUrl: {
249
+ label: 'API URL',
250
+ placeholder: 'Enter API URL',
251
+ },
252
+ },
253
+ };
254
+
181
255
  // Create locale files for each locale
182
256
  for (const locale of locales) {
183
257
  const localePath = `src/locales/${locale}/${pluginId}.json`;
184
258
 
185
259
  if (!context.doesFileExist(localePath)) {
186
- context.addFile(localePath, JSON.stringify({}, null, 2));
187
- additionsDebug(`Created ${localePath}`);
260
+ context.addFile(localePath, JSON.stringify(exampleTranslations, null, 2));
261
+ additionsDebug(`Created ${localePath} with example translations`);
188
262
  }
189
263
  }
190
264
  } catch (error) {
@@ -193,8 +267,14 @@ function createLocaleFiles(context: Context, locales: string[]): void {
193
267
  }
194
268
 
195
269
  function addI18nDependency(context: Context): void {
196
- addDependenciesToPackageJson(context, { '@grafana/i18n': '^1.0.0' }, {});
197
- additionsDebug('Added @grafana/i18n dependency');
270
+ addDependenciesToPackageJson(context, { '@grafana/i18n': '12.2.2' }, {});
271
+ additionsDebug('Added @grafana/i18n dependency version 12.2.2');
272
+ }
273
+
274
+ function addSemverDependency(context: Context): void {
275
+ // Add semver as regular dependency and @types/semver as dev dependency for backward compatibility
276
+ addDependenciesToPackageJson(context, { semver: '^7.6.0' }, { '@types/semver': '^7.5.0' });
277
+ additionsDebug('Added semver dependency for backward compatibility');
198
278
  }
199
279
 
200
280
  function addI18nextCli(context: Context): void {
@@ -355,7 +435,7 @@ export default defineConfig({
355
435
  }
356
436
  }
357
437
 
358
- function addI18nInitialization(context: Context): void {
438
+ function addI18nInitialization(context: Context, needsBackwardCompatibility: boolean): void {
359
439
  // Find module.ts or module.tsx
360
440
  const moduleTsPath = context.doesFileExist('src/module.ts')
361
441
  ? 'src/module.ts'
@@ -374,7 +454,7 @@ function addI18nInitialization(context: Context): void {
374
454
  }
375
455
 
376
456
  // Check if i18n is already initialized
377
- if (moduleContent.includes('@grafana/i18n')) {
457
+ if (moduleContent.includes('initPluginTranslations')) {
378
458
  additionsDebug('i18n already initialized in module file');
379
459
  return;
380
460
  }
@@ -384,18 +464,72 @@ function addI18nInitialization(context: Context): void {
384
464
  parser: require('recast/parsers/babel-ts'),
385
465
  });
386
466
 
387
- // Add import for i18n
388
- const i18nImport = builders.importDeclaration(
389
- [builders.importSpecifier(builders.identifier('i18n'))],
390
- builders.literal('@grafana/i18n')
467
+ const imports = [];
468
+
469
+ // Add necessary imports based on backward compatibility
470
+ imports.push(
471
+ builders.importDeclaration(
472
+ [builders.importSpecifier(builders.identifier('initPluginTranslations'))],
473
+ builders.literal('@grafana/i18n')
474
+ )
475
+ );
476
+
477
+ imports.push(
478
+ builders.importDeclaration(
479
+ [builders.importDefaultSpecifier(builders.identifier('pluginJson'))],
480
+ builders.literal('plugin.json')
481
+ )
391
482
  );
392
483
 
393
- // Add the import after the first import statement
484
+ if (needsBackwardCompatibility) {
485
+ imports.push(
486
+ builders.importDeclaration(
487
+ [builders.importSpecifier(builders.identifier('config'))],
488
+ builders.literal('@grafana/runtime')
489
+ )
490
+ );
491
+ imports.push(
492
+ builders.importDeclaration(
493
+ [builders.importDefaultSpecifier(builders.identifier('semver'))],
494
+ builders.literal('semver')
495
+ )
496
+ );
497
+ imports.push(
498
+ builders.importDeclaration(
499
+ [builders.importSpecifier(builders.identifier('loadResources'))],
500
+ builders.literal('./loadResources')
501
+ )
502
+ );
503
+ }
504
+
505
+ // Add imports after the first import statement
394
506
  const firstImportIndex = ast.program.body.findIndex((node: any) => node.type === 'ImportDeclaration');
395
507
  if (firstImportIndex !== -1) {
396
- ast.program.body.splice(firstImportIndex + 1, 0, i18nImport);
508
+ ast.program.body.splice(firstImportIndex + 1, 0, ...imports);
397
509
  } else {
398
- ast.program.body.unshift(i18nImport);
510
+ ast.program.body.unshift(...imports);
511
+ }
512
+
513
+ // Add i18n initialization code
514
+ const i18nInitCode = needsBackwardCompatibility
515
+ ? `// Before Grafana version 12.1.0 the plugin is responsible for loading translation resources
516
+ // In Grafana version 12.1.0 and later Grafana is responsible for loading translation resources
517
+ const loaders = semver.lt(config?.buildInfo?.version, '12.1.0') ? [loadResources] : [];
518
+
519
+ await initPluginTranslations(pluginJson.id, loaders);`
520
+ : `await initPluginTranslations(pluginJson.id);`;
521
+
522
+ // Parse the initialization code and insert it at the top level (after imports)
523
+ const initAst = recast.parse(i18nInitCode, {
524
+ parser: require('recast/parsers/babel-ts'),
525
+ });
526
+
527
+ // Find the last import index
528
+ const lastImportIndex = ast.program.body.findLastIndex((node: any) => node.type === 'ImportDeclaration');
529
+ if (lastImportIndex !== -1) {
530
+ ast.program.body.splice(lastImportIndex + 1, 0, ...initAst.program.body);
531
+ } else {
532
+ ast.program.body.unshift(...initAst.program.body);
399
533
  }
400
534
 
401
535
  const output = recast.print(ast, {
@@ -405,8 +539,46 @@ function addI18nInitialization(context: Context): void {
405
539
  }).code;
406
540
 
407
541
  context.updateFile(moduleTsPath, output);
408
- additionsDebug(`Updated ${moduleTsPath} with i18n import`);
542
+ additionsDebug(`Updated ${moduleTsPath} with i18n initialization`);
409
543
  } catch (error) {
410
544
  additionsDebug('Error updating module file:', error);
411
545
  }
412
546
  }
547
+
548
+ function createLoadResourcesFile(context: Context): void {
549
+ const loadResourcesPath = 'src/loadResources.ts';
550
+
551
+ if (context.doesFileExist(loadResourcesPath)) {
552
+ additionsDebug('loadResources.ts already exists, skipping');
553
+ return;
554
+ }
555
+
556
+ const pluginJsonRaw = context.getFile('src/plugin.json');
557
+ if (!pluginJsonRaw) {
558
+ additionsDebug('Cannot create loadResources.ts without plugin.json');
559
+ return;
560
+ }
561
+
562
+ const loadResourcesContent = `import { LANGUAGES, ResourceLoader, Resources } from '@grafana/i18n';
563
+ import pluginJson from 'plugin.json';
564
+
565
+ const resources = LANGUAGES.reduce<Record<string, () => Promise<{ default: Resources }>>>((acc, lang) => {
566
+ acc[lang.code] = async () => await import(\`./locales/\${lang.code}/\${pluginJson.id}.json\`);
567
+ return acc;
568
+ }, {});
569
+
570
+ export const loadResources: ResourceLoader = async (resolvedLanguage: string) => {
571
+ try {
572
+ const translation = await resources[resolvedLanguage]();
573
+ return translation.default;
574
+ } catch (error) {
575
+ // This makes sure that the plugin doesn't crash when the resolved language in Grafana isn't supported by the plugin
576
+ console.error(\`The plugin '\${pluginJson.id}' doesn't support the language '\${resolvedLanguage}'\`, error);
577
+ return {};
578
+ }
579
+ };
580
+ `;
581
+
582
+ context.addFile(loadResourcesPath, loadResourcesContent);
583
+ additionsDebug('Created src/loadResources.ts for backward compatibility');
584
+ }