@grafana/create-plugin 6.5.0-canary.2320.20268447181.0 → 6.5.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,102 +0,0 @@
1
- import * as v from 'valibot';
2
-
3
- import type { Context } from '../../../context.js';
4
- import { additionsDebug } from '../../../utils.js';
5
- import { updateDockerCompose, updatePluginJson, createI18nextConfig, ensureI18nextExternal } from './config-updates.js';
6
- import { addI18nInitialization, createLoadResourcesFile } from './code-generation.js';
7
- import { updateEslintConfig, addI18nDependency, addSemverDependency, addI18nextCli } from './tooling.js';
8
- import { checkNeedsBackwardCompatibility, createLocaleFiles, checkReactVersion } from './utils.js';
9
-
10
- /**
11
- * I18n addition schema using Valibot
12
- * Adds internationalization support to a plugin
13
- */
14
- export const schema = v.object(
15
- {
16
- locales: v.pipe(
17
- v.union([v.string(), v.array(v.string())]),
18
- v.transform((input) => {
19
- // Handle both string (from CLI) and array (from tests)
20
- return typeof input === 'string' ? input.split(',').map((s) => s.trim()) : input;
21
- }),
22
- v.array(
23
- v.pipe(
24
- v.string(),
25
- v.regex(/^[a-z]{2}-[A-Z]{2}$/, 'Locale must be in format xx-XX (e.g., en-US, es-ES, sv-SE)')
26
- ),
27
- 'Please provide a comma-separated list of all supported locales, e.g., "en-US,es-ES,sv-SE"'
28
- ),
29
- v.minLength(1, 'Please provide a comma-separated list of all supported locales, e.g., "en-US,es-ES,sv-SE"')
30
- ),
31
- },
32
- 'Please provide a comma-separated list of all supported locales, e.g., "en-US,es-ES,sv-SE"'
33
- );
34
-
35
- type I18nOptions = v.InferOutput<typeof schema>;
36
-
37
- export default function i18nAddition(context: Context, options: I18nOptions): Context {
38
- const { locales } = options;
39
-
40
- additionsDebug('Adding i18n support with locales:', locales);
41
-
42
- // Check React version early - @grafana/i18n requires React 18+
43
- checkReactVersion(context);
44
-
45
- // Determine if we need backward compatibility (Grafana < 12.1.0)
46
- const needsBackwardCompatibility = checkNeedsBackwardCompatibility(context);
47
- additionsDebug('Needs backward compatibility:', needsBackwardCompatibility);
48
-
49
- // 1. Update docker-compose.yaml with feature toggle
50
- updateDockerCompose(context);
51
-
52
- // 2. Update plugin.json with languages and grafanaDependency
53
- updatePluginJson(context, locales, needsBackwardCompatibility);
54
-
55
- // 3. Create locale folders and files
56
- createLocaleFiles(context, locales);
57
-
58
- // 4. Add @grafana/i18n dependency
59
- addI18nDependency(context);
60
-
61
- // 5. Add semver dependency for backward compatibility
62
- if (needsBackwardCompatibility) {
63
- addSemverDependency(context);
64
- }
65
-
66
- // 6. Update eslint.config.mjs if needed
67
- updateEslintConfig(context);
68
-
69
- // 7. Add i18n initialization to module file
70
- addI18nInitialization(context, needsBackwardCompatibility);
71
-
72
- // 8. Create loadResources.ts for backward compatibility
73
- if (needsBackwardCompatibility) {
74
- createLoadResourcesFile(context);
75
- }
76
-
77
- // 9. Add i18next-cli as dev dependency and add script
78
- addI18nextCli(context);
79
-
80
- // 10. Create i18next.config.ts
81
- createI18nextConfig(context);
82
-
83
- // 11. Ensure i18next is in externals array
84
- try {
85
- ensureI18nextExternal(context);
86
- } catch (error) {
87
- additionsDebug(`Error ensuring i18next external: ${error instanceof Error ? error.message : String(error)}`);
88
- }
89
-
90
- // Success message with next steps
91
- additionsDebug('\n✅ i18n support has been successfully added to your plugin!\n');
92
- additionsDebug('Next steps:');
93
- additionsDebug('1. Follow the instructions to translate your source code:');
94
- additionsDebug(
95
- ' https://grafana.com/developers/plugin-tools/how-to-guides/plugin-internationalization-grafana-11#determine-the-text-to-translate'
96
- );
97
- additionsDebug('2. Run the i18n-extract script to scan your code for translatable strings:');
98
- additionsDebug(' npm run i18n-extract (or yarn/pnpm run i18n-extract)');
99
- additionsDebug('3. Fill in your locale JSON files with translated strings\n');
100
-
101
- return context;
102
- }
@@ -1,80 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { Context } from '../../../context.js';
4
- import { updateEslintConfig } from './tooling.js';
5
-
6
- describe('tooling', () => {
7
- describe('updateEslintConfig', () => {
8
- it('should add correct ESLint config with proper rules and options', () => {
9
- const context = new Context('/virtual');
10
-
11
- context.addFile(
12
- 'eslint.config.mjs',
13
- 'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
14
- );
15
-
16
- updateEslintConfig(context);
17
-
18
- const eslintConfig = context.getFile('eslint.config.mjs');
19
-
20
- // Check correct import (recast uses double quotes)
21
- expect(eslintConfig).toContain('import grafanaI18nPlugin from "@grafana/i18n/eslint-plugin"');
22
-
23
- // Check plugin registration
24
- expect(eslintConfig).toContain('"@grafana/i18n": grafanaI18nPlugin');
25
-
26
- // Check rules are present
27
- expect(eslintConfig).toContain('"@grafana/i18n/no-untranslated-strings"');
28
- expect(eslintConfig).toContain('"@grafana/i18n/no-translation-top-level"');
29
-
30
- // Check rule configuration
31
- expect(eslintConfig).toContain('"error"');
32
- expect(eslintConfig).toContain('calleesToIgnore');
33
- expect(eslintConfig).toContain('"^css$"');
34
- expect(eslintConfig).toContain('"use[A-Z].*"');
35
-
36
- // Check config name
37
- expect(eslintConfig).toContain('name: "grafana/i18n-rules"');
38
- });
39
-
40
- it('should not add ESLint config if already present', () => {
41
- const context = new Context('/virtual');
42
-
43
- context.addFile(
44
- 'eslint.config.mjs',
45
- 'import { defineConfig } from "eslint/config";\nimport grafanaI18nPlugin from "@grafana/i18n/eslint-plugin";\nexport default defineConfig([]);'
46
- );
47
-
48
- const originalContent = context.getFile('eslint.config.mjs');
49
-
50
- updateEslintConfig(context);
51
-
52
- // The ESLint config should remain unchanged
53
- const eslintConfig = context.getFile('eslint.config.mjs');
54
- expect(eslintConfig).toBe(originalContent);
55
- expect(eslintConfig).toContain('@grafana/i18n/eslint-plugin');
56
- // Should not have duplicate imports or configs
57
- const importCount = (eslintConfig?.match(/@grafana\/i18n\/eslint-plugin/g) || []).length;
58
- expect(importCount).toBe(1);
59
- });
60
-
61
- it('should handle missing eslint.config.mjs gracefully', () => {
62
- const context = new Context('/virtual');
63
- // No eslint.config.mjs file
64
-
65
- expect(() => {
66
- updateEslintConfig(context);
67
- }).not.toThrow();
68
- });
69
-
70
- it('should handle empty eslint.config.mjs gracefully', () => {
71
- const context = new Context('/virtual');
72
-
73
- context.addFile('eslint.config.mjs', '');
74
-
75
- expect(() => {
76
- updateEslintConfig(context);
77
- }).not.toThrow();
78
- });
79
- });
80
- });
@@ -1,150 +0,0 @@
1
- import * as recast from 'recast';
2
- import * as babelParser from 'recast/parsers/babel-ts.js';
3
-
4
- import type { Context } from '../../../context.js';
5
- import { addDependenciesToPackageJson, additionsDebug } from '../../../utils.js';
6
-
7
- const { builders } = recast.types;
8
-
9
- export function addI18nDependency(context: Context): void {
10
- addDependenciesToPackageJson(context, { '@grafana/i18n': '^12.2.2' }, {});
11
- additionsDebug('Added @grafana/i18n dependency version ^12.2.2');
12
- }
13
-
14
- export function addSemverDependency(context: Context): void {
15
- // Add semver as regular dependency and @types/semver as dev dependency
16
- addDependenciesToPackageJson(context, { semver: '^7.6.0' }, { '@types/semver': '^7.5.0' });
17
- additionsDebug('Added semver dependency');
18
- }
19
-
20
- export function addI18nextCli(context: Context): void {
21
- // Add i18next-cli as dev dependency
22
- addDependenciesToPackageJson(context, {}, { 'i18next-cli': '^1.1.1' });
23
-
24
- // Add i18n-extract script to package.json
25
- const packageJsonRaw = context.getFile('package.json');
26
- if (!packageJsonRaw) {
27
- return;
28
- }
29
-
30
- try {
31
- const packageJson = JSON.parse(packageJsonRaw);
32
-
33
- if (!packageJson.scripts) {
34
- packageJson.scripts = {};
35
- }
36
-
37
- // Defensive: only add if not already present
38
- if (!packageJson.scripts['i18n-extract']) {
39
- packageJson.scripts['i18n-extract'] = 'i18next-cli extract --sync-primary';
40
- context.updateFile('package.json', JSON.stringify(packageJson, null, 2));
41
- additionsDebug('Added i18n-extract script to package.json');
42
- } else {
43
- additionsDebug('i18n-extract script already exists, skipping');
44
- }
45
- } catch (error) {
46
- additionsDebug('Error adding i18n-extract script:', error);
47
- }
48
- }
49
-
50
- export function updateEslintConfig(context: Context): void {
51
- if (!context.doesFileExist('eslint.config.mjs')) {
52
- additionsDebug('eslint.config.mjs not found, skipping');
53
- return;
54
- }
55
-
56
- const eslintConfigRaw = context.getFile('eslint.config.mjs');
57
- if (!eslintConfigRaw) {
58
- return;
59
- }
60
-
61
- // Defensive: check if @grafana/i18n eslint plugin is already configured
62
- if (eslintConfigRaw.includes('@grafana/i18n/eslint-plugin')) {
63
- additionsDebug('ESLint i18n rule already configured');
64
- return;
65
- }
66
-
67
- try {
68
- const ast = recast.parse(eslintConfigRaw, {
69
- parser: babelParser,
70
- });
71
-
72
- // Find the import section and add the plugin import
73
- const imports = ast.program.body.filter((node: any) => node.type === 'ImportDeclaration');
74
- const lastImport = imports[imports.length - 1];
75
-
76
- // Always create the plugin import
77
- const pluginImport = builders.importDeclaration(
78
- [builders.importDefaultSpecifier(builders.identifier('grafanaI18nPlugin'))],
79
- builders.literal('@grafana/i18n/eslint-plugin')
80
- );
81
-
82
- if (lastImport) {
83
- const lastImportIndex = ast.program.body.indexOf(lastImport);
84
- ast.program.body.splice(lastImportIndex + 1, 0, pluginImport);
85
- } else {
86
- // No imports found, insert at the beginning
87
- ast.program.body.unshift(pluginImport);
88
- }
89
-
90
- // Find the defineConfig array and add the plugin config
91
- recast.visit(ast, {
92
- visitCallExpression(path: any) {
93
- if (path.node.callee.name === 'defineConfig' && path.node.arguments[0]?.type === 'ArrayExpression') {
94
- const configArray = path.node.arguments[0];
95
-
96
- // Add the grafana i18n config object
97
- const i18nConfig = builders.objectExpression([
98
- builders.property('init', builders.identifier('name'), builders.literal('grafana/i18n-rules')),
99
- builders.property(
100
- 'init',
101
- builders.identifier('plugins'),
102
- builders.objectExpression([
103
- builders.property('init', builders.literal('@grafana/i18n'), builders.identifier('grafanaI18nPlugin')),
104
- ])
105
- ),
106
- builders.property(
107
- 'init',
108
- builders.identifier('rules'),
109
- builders.objectExpression([
110
- builders.property(
111
- 'init',
112
- builders.literal('@grafana/i18n/no-untranslated-strings'),
113
- builders.arrayExpression([
114
- builders.literal('error'),
115
- builders.objectExpression([
116
- builders.property(
117
- 'init',
118
- builders.identifier('calleesToIgnore'),
119
- builders.arrayExpression([builders.literal('^css$'), builders.literal('use[A-Z].*')])
120
- ),
121
- ]),
122
- ])
123
- ),
124
- builders.property(
125
- 'init',
126
- builders.literal('@grafana/i18n/no-translation-top-level'),
127
- builders.literal('error')
128
- ),
129
- ])
130
- ),
131
- ]);
132
-
133
- configArray.elements.push(i18nConfig);
134
- }
135
- this.traverse(path);
136
- },
137
- });
138
-
139
- const output = recast.print(ast, {
140
- tabWidth: 2,
141
- trailingComma: true,
142
- lineTerminator: '\n',
143
- }).code;
144
-
145
- context.updateFile('eslint.config.mjs', output);
146
- additionsDebug('Updated eslint.config.mjs with i18n linting rules');
147
- } catch (error) {
148
- additionsDebug('Error updating eslint.config.mjs:', error);
149
- }
150
- }
@@ -1,133 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { Context } from '../../../context.js';
4
- import { checkReactVersion } from './utils.js';
5
-
6
- describe('utils', () => {
7
- describe('checkReactVersion', () => {
8
- it('should throw error if React < 18 in dependencies', () => {
9
- const context = new Context('/virtual');
10
-
11
- context.addFile(
12
- 'package.json',
13
- JSON.stringify({
14
- dependencies: {
15
- react: '^17.0.2',
16
- 'react-dom': '^17.0.2',
17
- },
18
- })
19
- );
20
-
21
- expect(() => {
22
- checkReactVersion(context);
23
- }).toThrow('@grafana/i18n requires React 18 or higher');
24
- });
25
-
26
- it('should throw error if React 17 in devDependencies', () => {
27
- const context = new Context('/virtual');
28
-
29
- context.addFile(
30
- 'package.json',
31
- JSON.stringify({
32
- devDependencies: {
33
- react: '^17.0.2',
34
- 'react-dom': '^17.0.2',
35
- },
36
- })
37
- );
38
-
39
- expect(() => {
40
- checkReactVersion(context);
41
- }).toThrow('@grafana/i18n requires React 18 or higher');
42
- });
43
-
44
- it('should throw error if React 17 in peerDependencies', () => {
45
- const context = new Context('/virtual');
46
-
47
- context.addFile(
48
- 'package.json',
49
- JSON.stringify({
50
- peerDependencies: {
51
- react: '^17.0.2',
52
- 'react-dom': '^17.0.2',
53
- },
54
- })
55
- );
56
-
57
- expect(() => {
58
- checkReactVersion(context);
59
- }).toThrow('@grafana/i18n requires React 18 or higher');
60
- });
61
-
62
- it('should continue if React >= 18', () => {
63
- const context = new Context('/virtual');
64
-
65
- context.addFile(
66
- 'package.json',
67
- JSON.stringify({
68
- dependencies: {
69
- react: '^18.3.0',
70
- 'react-dom': '^18.3.0',
71
- },
72
- })
73
- );
74
-
75
- expect(() => {
76
- checkReactVersion(context);
77
- }).not.toThrow();
78
- });
79
-
80
- it('should continue if React version cannot be determined (no package.json)', () => {
81
- const context = new Context('/virtual');
82
- // No package.json file
83
-
84
- expect(() => {
85
- checkReactVersion(context);
86
- }).not.toThrow();
87
- });
88
-
89
- it('should continue if React version cannot be determined (no React dependency)', () => {
90
- const context = new Context('/virtual');
91
-
92
- context.addFile('package.json', JSON.stringify({})); // No React dependency
93
-
94
- expect(() => {
95
- checkReactVersion(context);
96
- }).not.toThrow();
97
- });
98
-
99
- it('should handle version ranges correctly', () => {
100
- const context = new Context('/virtual');
101
-
102
- context.addFile(
103
- 'package.json',
104
- JSON.stringify({
105
- dependencies: {
106
- react: '~18.1.0',
107
- },
108
- })
109
- );
110
-
111
- expect(() => {
112
- checkReactVersion(context);
113
- }).not.toThrow();
114
- });
115
-
116
- it('should handle React 19', () => {
117
- const context = new Context('/virtual');
118
-
119
- context.addFile(
120
- 'package.json',
121
- JSON.stringify({
122
- dependencies: {
123
- react: '^19.0.0',
124
- },
125
- })
126
- );
127
-
128
- expect(() => {
129
- checkReactVersion(context);
130
- }).not.toThrow();
131
- });
132
- });
133
- });
@@ -1,106 +0,0 @@
1
- import { coerce, gte } from 'semver';
2
-
3
- import type { Context } from '../../../context.js';
4
- import { additionsDebug } from '../../../utils.js';
5
-
6
- /**
7
- * Checks if React version is >= 18
8
- * @throws Error if React < 18 (since @grafana/i18n requires React 18+)
9
- */
10
- export function checkReactVersion(context: Context): void {
11
- const packageJsonRaw = context.getFile('package.json');
12
- if (!packageJsonRaw) {
13
- return;
14
- }
15
-
16
- try {
17
- const packageJson = JSON.parse(packageJsonRaw);
18
- const reactVersion =
19
- packageJson.dependencies?.react || packageJson.devDependencies?.react || packageJson.peerDependencies?.react;
20
-
21
- if (reactVersion) {
22
- const reactVersionStr = reactVersion.replace(/[^0-9.]/g, '');
23
- const reactVersionCoerced = coerce(reactVersionStr);
24
-
25
- if (reactVersionCoerced && !gte(reactVersionCoerced, '18.0.0')) {
26
- throw new Error(
27
- `@grafana/i18n requires React 18 or higher. Your plugin is using React ${reactVersion}.\n\n` +
28
- `Please upgrade to React 18+ to use i18n support.\n` +
29
- `Update your package.json to use "react": "^18.3.0" and "react-dom": "^18.3.0".`
30
- );
31
- }
32
- }
33
- } catch (error) {
34
- // If it's our version check error, re-throw it
35
- if (error instanceof Error && error.message.includes('@grafana/i18n requires React')) {
36
- throw error;
37
- }
38
- // Otherwise, just log and continue (can't determine React version)
39
- additionsDebug('Error checking React version:', error);
40
- }
41
- }
42
-
43
- export function checkNeedsBackwardCompatibility(context: Context): boolean {
44
- const pluginJsonRaw = context.getFile('src/plugin.json');
45
- if (!pluginJsonRaw) {
46
- // Default to backward compat for safety when plugin.json is missing
47
- return true;
48
- }
49
-
50
- try {
51
- const pluginJson = JSON.parse(pluginJsonRaw);
52
- const currentGrafanaDep = pluginJson.dependencies?.grafanaDependency;
53
-
54
- if (!currentGrafanaDep) {
55
- additionsDebug(
56
- 'Warning: grafanaDependency is missing from plugin.json. Assuming backward compatibility mode is needed.'
57
- );
58
- return true;
59
- }
60
-
61
- const minVersion = coerce('12.1.0');
62
- const currentVersion = coerce(currentGrafanaDep.replace(/^[><=]+/, ''));
63
-
64
- // If current version is less than 12.1.0, we need backward compatibility
65
- if (currentVersion && minVersion && gte(currentVersion, minVersion)) {
66
- return false; // Already >= 12.1.0, no backward compat needed
67
- }
68
- return true; // < 12.1.0, needs backward compat
69
- } catch (error) {
70
- additionsDebug('Error checking backward compatibility:', error);
71
- return true; // Default to backward compat on error
72
- }
73
- }
74
-
75
- export function createLocaleFiles(context: Context, locales: string[]): void {
76
- // Get plugin ID from plugin.json
77
- const pluginJsonRaw = context.getFile('src/plugin.json');
78
- if (!pluginJsonRaw) {
79
- additionsDebug('Cannot create locale files without plugin.json');
80
- return;
81
- }
82
-
83
- try {
84
- const pluginJson = JSON.parse(pluginJsonRaw);
85
- const pluginId = pluginJson.id;
86
-
87
- if (!pluginId) {
88
- additionsDebug('No plugin ID found in plugin.json');
89
- return;
90
- }
91
-
92
- // Create locale files for each locale (defensive: only if not already present)
93
- for (const locale of locales) {
94
- const localePath = `src/locales/${locale}/${pluginId}.json`;
95
-
96
- if (!context.doesFileExist(localePath)) {
97
- context.addFile(localePath, JSON.stringify({}, null, 2));
98
- additionsDebug(`Created ${localePath}`);
99
- } else {
100
- additionsDebug(`${localePath} already exists, skipping`);
101
- }
102
- }
103
- } catch (error) {
104
- additionsDebug('Error creating locale files:', error);
105
- }
106
- }