@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,209 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { Context } from '../../../context.js';
4
- import { ensureI18nextExternal, updatePluginJson } from './config-updates.js';
5
-
6
- describe('config-updates', () => {
7
- describe('ensureI18nextExternal', () => {
8
- it('should add i18next to externals array in .config/bundler/externals.ts', () => {
9
- const context = new Context('/virtual');
10
-
11
- context.addFile('.config/bundler/externals.ts', `export const externals = ['react', 'react-dom'];`);
12
-
13
- ensureI18nextExternal(context);
14
-
15
- const externalsContent = context.getFile('.config/bundler/externals.ts');
16
- expect(externalsContent).toMatch(/["']i18next["']/);
17
- expect(externalsContent).toContain("'react'");
18
- expect(externalsContent).toContain("'react-dom'");
19
- });
20
-
21
- it('should not duplicate i18next if already in externals array', () => {
22
- const context = new Context('/virtual');
23
-
24
- const originalExternals = `export const externals = ['react', 'i18next', 'react-dom'];`;
25
- context.addFile('.config/bundler/externals.ts', originalExternals);
26
-
27
- ensureI18nextExternal(context);
28
-
29
- const externalsContent = context.getFile('.config/bundler/externals.ts');
30
- const i18nextCount = (externalsContent?.match(/["']i18next["']/g) || []).length;
31
- expect(i18nextCount).toBe(1);
32
- });
33
-
34
- it('should add i18next to externals in .config/webpack/webpack.config.ts (legacy)', () => {
35
- const context = new Context('/virtual');
36
-
37
- context.addFile(
38
- '.config/webpack/webpack.config.ts',
39
- `import { Configuration } from 'webpack';
40
- export const config: Configuration = {
41
- externals: ['react', 'react-dom'],
42
- };`
43
- );
44
-
45
- ensureI18nextExternal(context);
46
-
47
- const webpackConfig = context.getFile('.config/webpack/webpack.config.ts');
48
- expect(webpackConfig).toMatch(/["']i18next["']/);
49
- expect(webpackConfig).toContain("'react'");
50
- expect(webpackConfig).toContain("'react-dom'");
51
- });
52
-
53
- it('should handle missing externals configuration gracefully', () => {
54
- const context = new Context('/virtual');
55
- // No externals.ts or webpack.config.ts
56
-
57
- expect(() => {
58
- ensureI18nextExternal(context);
59
- }).not.toThrow();
60
- });
61
-
62
- it('should prefer .config/bundler/externals.ts over webpack.config.ts', () => {
63
- const context = new Context('/virtual');
64
-
65
- context.addFile('.config/bundler/externals.ts', `export const externals = ['react'];`);
66
- context.addFile('.config/webpack/webpack.config.ts', `export const config = { externals: ['react-dom'] };`);
67
-
68
- ensureI18nextExternal(context);
69
-
70
- // Should update externals.ts, not webpack.config.ts
71
- const externalsContent = context.getFile('.config/bundler/externals.ts');
72
- expect(externalsContent).toMatch(/["']i18next["']/);
73
-
74
- const webpackConfig = context.getFile('.config/webpack/webpack.config.ts');
75
- expect(webpackConfig).not.toMatch(/["']i18next["']/);
76
- });
77
- });
78
-
79
- describe('updatePluginJson', () => {
80
- it('should auto-update grafanaDependency from < 11.0.0 to >=11.0.0', () => {
81
- const context = new Context('/virtual');
82
-
83
- context.addFile(
84
- 'src/plugin.json',
85
- JSON.stringify({
86
- id: 'test-plugin',
87
- type: 'panel',
88
- name: 'Test Plugin',
89
- dependencies: {
90
- grafanaDependency: '>=10.0.0',
91
- },
92
- })
93
- );
94
-
95
- updatePluginJson(context, ['en-US'], true);
96
-
97
- const pluginJson = JSON.parse(context.getFile('src/plugin.json') || '{}');
98
- expect(pluginJson.dependencies.grafanaDependency).toBe('>=11.0.0');
99
- expect(pluginJson.languages).toEqual(['en-US']);
100
- });
101
-
102
- it('should keep grafanaDependency >= 11.0.0 unchanged when needsBackwardCompatibility is true', () => {
103
- const context = new Context('/virtual');
104
-
105
- context.addFile(
106
- 'src/plugin.json',
107
- JSON.stringify({
108
- id: 'test-plugin',
109
- type: 'panel',
110
- name: 'Test Plugin',
111
- dependencies: {
112
- grafanaDependency: '>=11.0.0',
113
- },
114
- })
115
- );
116
-
117
- updatePluginJson(context, ['en-US'], true);
118
-
119
- const pluginJson = JSON.parse(context.getFile('src/plugin.json') || '{}');
120
- expect(pluginJson.dependencies.grafanaDependency).toBe('>=11.0.0');
121
- });
122
-
123
- it('should update grafanaDependency to >=12.1.0 when needsBackwardCompatibility is false', () => {
124
- const context = new Context('/virtual');
125
-
126
- context.addFile(
127
- 'src/plugin.json',
128
- JSON.stringify({
129
- id: 'test-plugin',
130
- type: 'panel',
131
- name: 'Test Plugin',
132
- dependencies: {
133
- grafanaDependency: '>=11.0.0',
134
- },
135
- })
136
- );
137
-
138
- updatePluginJson(context, ['en-US'], false);
139
-
140
- const pluginJson = JSON.parse(context.getFile('src/plugin.json') || '{}');
141
- expect(pluginJson.dependencies.grafanaDependency).toBe('>=12.1.0');
142
- });
143
-
144
- it('should merge locales with existing languages', () => {
145
- const context = new Context('/virtual');
146
-
147
- context.addFile(
148
- 'src/plugin.json',
149
- JSON.stringify({
150
- id: 'test-plugin',
151
- type: 'panel',
152
- name: 'Test Plugin',
153
- languages: ['en-US'],
154
- dependencies: {
155
- grafanaDependency: '>=12.1.0',
156
- },
157
- })
158
- );
159
-
160
- updatePluginJson(context, ['es-ES', 'sv-SE'], false);
161
-
162
- const pluginJson = JSON.parse(context.getFile('src/plugin.json') || '{}');
163
- expect(pluginJson.languages).toEqual(['en-US', 'es-ES', 'sv-SE']);
164
- });
165
-
166
- it('should not duplicate locales', () => {
167
- const context = new Context('/virtual');
168
-
169
- context.addFile(
170
- 'src/plugin.json',
171
- JSON.stringify({
172
- id: 'test-plugin',
173
- type: 'panel',
174
- name: 'Test Plugin',
175
- languages: ['en-US', 'es-ES'],
176
- dependencies: {
177
- grafanaDependency: '>=12.1.0',
178
- },
179
- })
180
- );
181
-
182
- updatePluginJson(context, ['en-US', 'sv-SE'], false);
183
-
184
- const pluginJson = JSON.parse(context.getFile('src/plugin.json') || '{}');
185
- expect(pluginJson.languages).toEqual(['en-US', 'es-ES', 'sv-SE']);
186
- });
187
-
188
- it('should not update grafanaDependency if it is already >= target version', () => {
189
- const context = new Context('/virtual');
190
-
191
- context.addFile(
192
- 'src/plugin.json',
193
- JSON.stringify({
194
- id: 'test-plugin',
195
- type: 'panel',
196
- name: 'Test Plugin',
197
- dependencies: {
198
- grafanaDependency: '>=13.0.0',
199
- },
200
- })
201
- );
202
-
203
- updatePluginJson(context, ['en-US'], false);
204
-
205
- const pluginJson = JSON.parse(context.getFile('src/plugin.json') || '{}');
206
- expect(pluginJson.dependencies.grafanaDependency).toBe('>=13.0.0');
207
- });
208
- });
209
- });
@@ -1,335 +0,0 @@
1
- import { coerce, gte } from 'semver';
2
- import { parseDocument, stringify } from 'yaml';
3
- import * as recast from 'recast';
4
- import * as typeScriptParser from 'recast/parsers/typescript.js';
5
-
6
- import type { Context } from '../../../context.js';
7
- import { additionsDebug } from '../../../utils.js';
8
-
9
- const { builders } = recast.types;
10
-
11
- export function updateDockerCompose(context: Context): void {
12
- if (!context.doesFileExist('docker-compose.yaml')) {
13
- additionsDebug('docker-compose.yaml not found, skipping');
14
- return;
15
- }
16
-
17
- const composeContent = context.getFile('docker-compose.yaml');
18
- if (!composeContent) {
19
- return;
20
- }
21
-
22
- try {
23
- const composeDoc = parseDocument(composeContent);
24
- const currentEnv = composeDoc.getIn(['services', 'grafana', 'environment']);
25
-
26
- if (!currentEnv) {
27
- additionsDebug('No environment section found in docker-compose.yaml, skipping');
28
- return;
29
- }
30
-
31
- // Check if the feature toggle is already set
32
- if (typeof currentEnv === 'object') {
33
- const envMap = currentEnv as any;
34
- const toggleValue = envMap.get('GF_FEATURE_TOGGLES_ENABLE');
35
-
36
- if (toggleValue) {
37
- const toggleStr = toggleValue.toString();
38
- if (toggleStr.includes('localizationForPlugins')) {
39
- additionsDebug('localizationForPlugins already in GF_FEATURE_TOGGLES_ENABLE');
40
- return;
41
- }
42
- // Append to existing feature toggles
43
- composeDoc.setIn(
44
- ['services', 'grafana', 'environment', 'GF_FEATURE_TOGGLES_ENABLE'],
45
- `${toggleStr},localizationForPlugins`
46
- );
47
- } else {
48
- // Set new feature toggle
49
- composeDoc.setIn(['services', 'grafana', 'environment', 'GF_FEATURE_TOGGLES_ENABLE'], 'localizationForPlugins');
50
- }
51
-
52
- context.updateFile('docker-compose.yaml', stringify(composeDoc));
53
- additionsDebug('Updated docker-compose.yaml with localizationForPlugins feature toggle');
54
- }
55
- } catch (error) {
56
- additionsDebug('Error updating docker-compose.yaml:', error);
57
- }
58
- }
59
-
60
- export function updatePluginJson(context: Context, locales: string[], needsBackwardCompatibility: boolean): void {
61
- if (!context.doesFileExist('src/plugin.json')) {
62
- additionsDebug('src/plugin.json not found, skipping');
63
- return;
64
- }
65
-
66
- const pluginJsonRaw = context.getFile('src/plugin.json');
67
- if (!pluginJsonRaw) {
68
- return;
69
- }
70
-
71
- try {
72
- const pluginJson = JSON.parse(pluginJsonRaw);
73
-
74
- // Merge locales with existing languages (defensive: avoid duplicates)
75
- const existingLanguages = Array.isArray(pluginJson.languages) ? pluginJson.languages : [];
76
- const mergedLanguages = [...new Set([...existingLanguages, ...locales])];
77
- pluginJson.languages = mergedLanguages;
78
-
79
- // Update grafanaDependency based on backward compatibility needs
80
- if (!pluginJson.dependencies) {
81
- pluginJson.dependencies = {};
82
- }
83
-
84
- const currentGrafanaDep = pluginJson.dependencies.grafanaDependency || '>=11.0.0';
85
- const targetVersion = needsBackwardCompatibility ? '11.0.0' : '12.1.0';
86
- const minVersion = coerce(targetVersion);
87
- const currentVersion = coerce(currentGrafanaDep.replace(/^[><=]+/, ''));
88
-
89
- if (!currentVersion || !minVersion || !gte(currentVersion, minVersion)) {
90
- pluginJson.dependencies.grafanaDependency = `>=${targetVersion}`;
91
- additionsDebug(`Updated grafanaDependency to >=${targetVersion}`);
92
- }
93
-
94
- context.updateFile('src/plugin.json', JSON.stringify(pluginJson, null, 2));
95
- additionsDebug('Updated src/plugin.json with languages:', locales);
96
- } catch (error) {
97
- additionsDebug('Error updating src/plugin.json:', error);
98
- }
99
- }
100
-
101
- export function createI18nextConfig(context: Context): void {
102
- // Defensive: skip if already exists
103
- if (context.doesFileExist('i18next.config.ts')) {
104
- additionsDebug('i18next.config.ts already exists, skipping');
105
- return;
106
- }
107
-
108
- const pluginJsonRaw = context.getFile('src/plugin.json');
109
- if (!pluginJsonRaw) {
110
- additionsDebug('Cannot create i18next.config.ts without plugin.json');
111
- return;
112
- }
113
-
114
- try {
115
- const pluginJson = JSON.parse(pluginJsonRaw);
116
- const pluginId = pluginJson.id;
117
-
118
- if (!pluginId) {
119
- additionsDebug('No plugin ID found in plugin.json');
120
- return;
121
- }
122
-
123
- const i18nextConfig = `import { defineConfig } from 'i18next-cli';
124
- import pluginJson from './src/plugin.json';
125
-
126
- export default defineConfig({
127
- locales: pluginJson.languages,
128
- extract: {
129
- input: ['src/**/*.{tsx,ts}'],
130
- output: 'src/locales/{{language}}/{{namespace}}.json',
131
- defaultNS: pluginJson.id,
132
- functions: ['t', '*.t'],
133
- transComponents: ['Trans'],
134
- },
135
- });
136
- `;
137
-
138
- context.addFile('i18next.config.ts', i18nextConfig);
139
- additionsDebug('Created i18next.config.ts');
140
- } catch (error) {
141
- additionsDebug('Error creating i18next.config.ts:', error);
142
- }
143
- }
144
-
145
- /**
146
- * Adds 'i18next' to an externals array if it's not already present
147
- * @returns true if changes were made, false otherwise
148
- */
149
- function addI18nextToExternalsArray(externalsArray: recast.types.namedTypes.ArrayExpression): boolean {
150
- // Check if 'i18next' is already in the array
151
- const hasI18next = externalsArray.elements.some((element) => {
152
- if (
153
- element &&
154
- (element.type === 'Literal' || element.type === 'StringLiteral') &&
155
- typeof element.value === 'string'
156
- ) {
157
- return element.value === 'i18next';
158
- }
159
- return false;
160
- });
161
-
162
- if (hasI18next) {
163
- additionsDebug("'i18next' already in externals array");
164
- return false;
165
- }
166
-
167
- // Append 'i18next' to the end of the array
168
- externalsArray.elements.push(builders.literal('i18next'));
169
- additionsDebug("Added 'i18next' to externals array");
170
- return true;
171
- }
172
-
173
- export function ensureI18nextExternal(context: Context): void {
174
- try {
175
- additionsDebug('Checking for externals configuration...');
176
-
177
- // Try new structure first: .config/bundler/externals.ts
178
- const externalsPath = '.config/bundler/externals.ts';
179
- if (context.doesFileExist(externalsPath)) {
180
- additionsDebug(`Found ${externalsPath}, checking for i18next...`);
181
- const externalsContent = context.getFile(externalsPath);
182
- if (externalsContent) {
183
- try {
184
- const ast = recast.parse(externalsContent, {
185
- parser: typeScriptParser,
186
- });
187
-
188
- let hasChanges = false;
189
-
190
- // Find the externals array
191
- recast.visit(ast, {
192
- visitVariableDeclarator(path) {
193
- const { node } = path;
194
-
195
- if (
196
- node.id.type === 'Identifier' &&
197
- node.id.name === 'externals' &&
198
- node.init &&
199
- node.init.type === 'ArrayExpression'
200
- ) {
201
- additionsDebug('Found externals array in externals.ts');
202
- if (addI18nextToExternalsArray(node.init)) {
203
- hasChanges = true;
204
- }
205
- }
206
-
207
- return this.traverse(path);
208
- },
209
- });
210
-
211
- // Only update the file if we made changes
212
- if (hasChanges) {
213
- const output = recast.print(ast, {
214
- tabWidth: 2,
215
- trailingComma: true,
216
- lineTerminator: '\n',
217
- });
218
- context.updateFile(externalsPath, output.code);
219
- additionsDebug(`Updated ${externalsPath} with i18next external`);
220
- }
221
- return;
222
- } catch (error) {
223
- additionsDebug(`Error updating ${externalsPath}:`, error);
224
- }
225
- }
226
- }
227
-
228
- // Fall back to legacy structure: .config/webpack/webpack.config.ts with inline externals
229
- const webpackConfigPath = '.config/webpack/webpack.config.ts';
230
- additionsDebug(`Checking for ${webpackConfigPath}...`);
231
- if (context.doesFileExist(webpackConfigPath)) {
232
- additionsDebug(`Found ${webpackConfigPath}, checking for inline externals...`);
233
- const webpackContent = context.getFile(webpackConfigPath);
234
- if (webpackContent) {
235
- try {
236
- const ast = recast.parse(webpackContent, {
237
- parser: typeScriptParser,
238
- });
239
-
240
- let hasChanges = false;
241
- let foundExternals = false;
242
-
243
- // Find the externals property in the Configuration object
244
- // It can be in baseConfig or any variable with an object initializer
245
- recast.visit(ast, {
246
- visitObjectExpression(path) {
247
- const { node } = path;
248
- const properties = node.properties;
249
-
250
- if (properties) {
251
- for (const prop of properties) {
252
- // Handle both Property and ObjectProperty types
253
- if (prop && (prop.type === 'Property' || prop.type === 'ObjectProperty')) {
254
- const key = 'key' in prop ? prop.key : null;
255
- const value = 'value' in prop ? prop.value : null;
256
-
257
- if (
258
- key &&
259
- key.type === 'Identifier' &&
260
- key.name === 'externals' &&
261
- value &&
262
- value.type === 'ArrayExpression'
263
- ) {
264
- foundExternals = true;
265
- additionsDebug('Found externals property in webpack.config.ts');
266
- if (addI18nextToExternalsArray(value)) {
267
- hasChanges = true;
268
- }
269
- // Don't break, continue to check all object expressions
270
- }
271
- }
272
- }
273
- }
274
-
275
- return this.traverse(path);
276
- },
277
- visitProperty(path) {
278
- const { node } = path;
279
-
280
- // Also check properties directly (fallback)
281
- if (
282
- node.key &&
283
- node.key.type === 'Identifier' &&
284
- node.key.name === 'externals' &&
285
- node.value &&
286
- node.value.type === 'ArrayExpression'
287
- ) {
288
- if (!foundExternals) {
289
- foundExternals = true;
290
- additionsDebug('Found externals property in webpack.config.ts (via visitProperty)');
291
- }
292
- if (addI18nextToExternalsArray(node.value)) {
293
- hasChanges = true;
294
- }
295
- }
296
-
297
- return this.traverse(path);
298
- },
299
- });
300
-
301
- if (!foundExternals) {
302
- additionsDebug('No externals property found in webpack.config.ts');
303
- }
304
-
305
- // Only update the file if we made changes
306
- if (hasChanges) {
307
- const output = recast.print(ast, {
308
- tabWidth: 2,
309
- trailingComma: true,
310
- lineTerminator: '\n',
311
- });
312
- context.updateFile(webpackConfigPath, output.code);
313
- additionsDebug(`Updated ${webpackConfigPath} with i18next external`);
314
- } else if (foundExternals) {
315
- additionsDebug('i18next already present in externals, no changes needed');
316
- }
317
- return;
318
- } catch (error) {
319
- additionsDebug(`Error updating ${webpackConfigPath}:`, error);
320
- additionsDebug(`Error details: ${error instanceof Error ? error.message : String(error)}`);
321
- }
322
- } else {
323
- additionsDebug(`File ${webpackConfigPath} exists but content is empty`);
324
- }
325
- } else {
326
- additionsDebug(`File ${webpackConfigPath} does not exist`);
327
- }
328
-
329
- additionsDebug('No externals configuration found, skipping i18next external check');
330
- } catch (error) {
331
- additionsDebug(
332
- `Unexpected error in ensureI18nextExternal: ${error instanceof Error ? error.message : String(error)}`
333
- );
334
- }
335
- }