@grafana/create-plugin 6.7.1-canary.2370.20798217827.0 → 6.8.0-canary.2356.20813241719.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,511 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { Context } from '../../../context.js';
4
+ import bundleGrafanaUI from './index.js';
5
+
6
+ const EXTERNALS_PATH = '.config/bundler/externals.ts';
7
+ const WEBPACK_CONFIG_PATH = '.config/webpack/webpack.config.ts';
8
+ const RSPACK_CONFIG_PATH = '.config/rspack/rspack.config.ts';
9
+ const PLUGIN_JSON_PATH = 'src/plugin.json';
10
+
11
+ const defaultExternalsContent = `import type { Configuration, ExternalItemFunctionData } from 'webpack';
12
+
13
+ type ExternalsType = Configuration['externals'];
14
+
15
+ export const externals: ExternalsType = [
16
+ { 'amd-module': 'module' },
17
+ 'lodash',
18
+ 'react',
19
+ 'react-dom',
20
+ /^@grafana\\/ui/i,
21
+ /^@grafana\\/runtime/i,
22
+ /^@grafana\\/data/i,
23
+ ];`;
24
+
25
+ const webpackConfigWithExternals = `import type { Configuration } from 'webpack';
26
+
27
+ const baseConfig: Configuration = {
28
+ externals: [
29
+ { 'amd-module': 'module' },
30
+ 'lodash',
31
+ 'react',
32
+ /^@grafana\\/ui/i,
33
+ /^@grafana\\/runtime/i,
34
+ /^@grafana\\/data/i,
35
+ ],
36
+ };
37
+
38
+ export default baseConfig;`;
39
+
40
+ describe('bundle-grafana-ui', () => {
41
+ describe('externals.ts (new structure)', () => {
42
+ it('should remove @grafana/ui from externals', () => {
43
+ const context = new Context('/virtual');
44
+ context.addFile(EXTERNALS_PATH, defaultExternalsContent);
45
+
46
+ const result = bundleGrafanaUI(context, {});
47
+
48
+ const content = result.getFile(EXTERNALS_PATH) || '';
49
+ expect(content).not.toMatch(/\/\^@grafana\\\/ui\/i/);
50
+ });
51
+
52
+ it('should add react-inlinesvg to externals', () => {
53
+ const context = new Context('/virtual');
54
+ context.addFile(EXTERNALS_PATH, defaultExternalsContent);
55
+
56
+ const result = bundleGrafanaUI(context, {});
57
+
58
+ const content = result.getFile(EXTERNALS_PATH) || '';
59
+ expect(content).toMatch(/['"]react-inlinesvg['"]/);
60
+ });
61
+
62
+ it('should preserve other externals', () => {
63
+ const context = new Context('/virtual');
64
+ context.addFile(EXTERNALS_PATH, defaultExternalsContent);
65
+
66
+ const result = bundleGrafanaUI(context, {});
67
+
68
+ const content = result.getFile(EXTERNALS_PATH) || '';
69
+ expect(content).toContain("'lodash'");
70
+ expect(content).toContain("'react'");
71
+ expect(content).toMatch(/\/\^@grafana\\\/runtime\/i/);
72
+ expect(content).toMatch(/\/\^@grafana\\\/data\/i/);
73
+ });
74
+
75
+ it('should be idempotent', () => {
76
+ const context = new Context('/virtual');
77
+ context.addFile(EXTERNALS_PATH, defaultExternalsContent);
78
+
79
+ const result1 = bundleGrafanaUI(context, {});
80
+ const content1 = result1.getFile(EXTERNALS_PATH) || '';
81
+
82
+ // Verify first run removed @grafana/ui and added react-inlinesvg
83
+ expect(content1).not.toContain('@grafana\\/ui');
84
+ expect(content1).toMatch(/['"]react-inlinesvg['"]/);
85
+
86
+ const context2 = new Context('/virtual');
87
+ context2.addFile(EXTERNALS_PATH, content1);
88
+ const result2 = bundleGrafanaUI(context2, {});
89
+
90
+ // Second run should produce identical content (idempotent)
91
+ const content2 = result2.getFile(EXTERNALS_PATH) || '';
92
+ expect(content2).toBe(content1);
93
+ });
94
+ });
95
+
96
+ describe('webpack.config.ts (legacy structure)', () => {
97
+ it('should remove @grafana/ui from externals in webpack config', () => {
98
+ const context = new Context('/virtual');
99
+ context.addFile(WEBPACK_CONFIG_PATH, webpackConfigWithExternals);
100
+
101
+ const result = bundleGrafanaUI(context, {});
102
+
103
+ const content = result.getFile(WEBPACK_CONFIG_PATH) || '';
104
+ expect(content).not.toMatch(/\/\^@grafana\\\/ui\/i/);
105
+ });
106
+
107
+ it('should add react-inlinesvg to externals in webpack config', () => {
108
+ const context = new Context('/virtual');
109
+ context.addFile(WEBPACK_CONFIG_PATH, webpackConfigWithExternals);
110
+
111
+ const result = bundleGrafanaUI(context, {});
112
+
113
+ const content = result.getFile(WEBPACK_CONFIG_PATH) || '';
114
+ expect(content).toMatch(/['"]react-inlinesvg['"]/);
115
+ });
116
+
117
+ it('should be idempotent for webpack config', () => {
118
+ const context = new Context('/virtual');
119
+ context.addFile(WEBPACK_CONFIG_PATH, webpackConfigWithExternals);
120
+
121
+ const result1 = bundleGrafanaUI(context, {});
122
+ const content1 = result1.getFile(WEBPACK_CONFIG_PATH) || '';
123
+
124
+ // Verify first run removed @grafana/ui and added react-inlinesvg
125
+ expect(content1).not.toContain('@grafana\\/ui');
126
+ expect(content1).toMatch(/['"]react-inlinesvg['"]/);
127
+
128
+ const context2 = new Context('/virtual');
129
+ context2.addFile(WEBPACK_CONFIG_PATH, content1);
130
+ const result2 = bundleGrafanaUI(context2, {});
131
+
132
+ // Second run should produce identical content (idempotent)
133
+ const content2 = result2.getFile(WEBPACK_CONFIG_PATH) || '';
134
+ expect(content2).toBe(content1);
135
+ });
136
+ });
137
+
138
+ describe('priority', () => {
139
+ it('should prefer externals.ts over webpack.config.ts when both exist', () => {
140
+ const context = new Context('/virtual');
141
+ context.addFile(EXTERNALS_PATH, defaultExternalsContent);
142
+ context.addFile(WEBPACK_CONFIG_PATH, webpackConfigWithExternals);
143
+
144
+ const result = bundleGrafanaUI(context, {});
145
+
146
+ // externals.ts should be updated
147
+ const externalsContent = result.getFile(EXTERNALS_PATH) || '';
148
+ expect(externalsContent).not.toMatch(/\/\^@grafana\\\/ui\/i/);
149
+ expect(externalsContent).toMatch(/['"]react-inlinesvg['"]/);
150
+
151
+ // webpack.config.ts should NOT be updated (still has @grafana/ui)
152
+ const webpackContent = result.getFile(WEBPACK_CONFIG_PATH) || '';
153
+ expect(webpackContent).toMatch(/\/\^@grafana\\\/ui\/i/);
154
+ });
155
+ });
156
+
157
+ describe('no config files', () => {
158
+ it('should do nothing if no config files exist', () => {
159
+ const context = new Context('/virtual');
160
+
161
+ const result = bundleGrafanaUI(context, {});
162
+
163
+ expect(result.hasChanges()).toBe(false);
164
+ });
165
+ });
166
+
167
+ describe('grafanaDependency version check', () => {
168
+ it('should bump grafanaDependency to 10.2.0 if lower', () => {
169
+ const context = new Context('/virtual');
170
+ context.addFile(EXTERNALS_PATH, defaultExternalsContent);
171
+ context.addFile(
172
+ PLUGIN_JSON_PATH,
173
+ JSON.stringify({
174
+ id: 'test-plugin',
175
+ dependencies: {
176
+ grafanaDependency: '>=9.0.0',
177
+ },
178
+ })
179
+ );
180
+
181
+ const result = bundleGrafanaUI(context, {});
182
+
183
+ const pluginJson = JSON.parse(result.getFile(PLUGIN_JSON_PATH) || '{}');
184
+ expect(pluginJson.dependencies.grafanaDependency).toBe('>=10.2.0');
185
+ });
186
+
187
+ it('should not change grafanaDependency if already >= 10.2.0', () => {
188
+ const context = new Context('/virtual');
189
+ context.addFile(EXTERNALS_PATH, defaultExternalsContent);
190
+ context.addFile(
191
+ PLUGIN_JSON_PATH,
192
+ JSON.stringify({
193
+ id: 'test-plugin',
194
+ dependencies: {
195
+ grafanaDependency: '>=11.0.0',
196
+ },
197
+ })
198
+ );
199
+
200
+ const result = bundleGrafanaUI(context, {});
201
+
202
+ const pluginJson = JSON.parse(result.getFile(PLUGIN_JSON_PATH) || '{}');
203
+ expect(pluginJson.dependencies.grafanaDependency).toBe('>=11.0.0');
204
+ });
205
+
206
+ it('should add dependencies object if missing', () => {
207
+ const context = new Context('/virtual');
208
+ context.addFile(EXTERNALS_PATH, defaultExternalsContent);
209
+ context.addFile(
210
+ PLUGIN_JSON_PATH,
211
+ JSON.stringify({
212
+ id: 'test-plugin',
213
+ })
214
+ );
215
+
216
+ const result = bundleGrafanaUI(context, {});
217
+
218
+ const pluginJson = JSON.parse(result.getFile(PLUGIN_JSON_PATH) || '{}');
219
+ expect(pluginJson.dependencies.grafanaDependency).toBe('>=10.2.0');
220
+ });
221
+
222
+ it('should handle version with exact match (10.2.0)', () => {
223
+ const context = new Context('/virtual');
224
+ context.addFile(EXTERNALS_PATH, defaultExternalsContent);
225
+ context.addFile(
226
+ PLUGIN_JSON_PATH,
227
+ JSON.stringify({
228
+ id: 'test-plugin',
229
+ dependencies: {
230
+ grafanaDependency: '>=10.2.0',
231
+ },
232
+ })
233
+ );
234
+
235
+ const result = bundleGrafanaUI(context, {});
236
+
237
+ const pluginJson = JSON.parse(result.getFile(PLUGIN_JSON_PATH) || '{}');
238
+ expect(pluginJson.dependencies.grafanaDependency).toBe('>=10.2.0');
239
+ });
240
+ });
241
+
242
+ describe('resolve configuration updates', () => {
243
+ const webpackConfigWithResolve = `import type { Configuration } from 'webpack';
244
+
245
+ const baseConfig: Configuration = {
246
+ module: {
247
+ rules: [],
248
+ },
249
+ resolve: {
250
+ extensions: ['.js', '.jsx', '.ts', '.tsx'],
251
+ modules: ['node_modules'],
252
+ },
253
+ };
254
+
255
+ export default baseConfig;`;
256
+
257
+ const rspackConfigWithResolve = `import type { Configuration } from '@rspack/core';
258
+
259
+ const baseConfig: Configuration = {
260
+ module: {
261
+ rules: [],
262
+ },
263
+ resolve: {
264
+ extensions: ['.js', '.jsx', '.ts', '.tsx'],
265
+ modules: ['node_modules'],
266
+ },
267
+ };
268
+
269
+ export default baseConfig;`;
270
+
271
+ describe('webpack.config.ts', () => {
272
+ it('should add .mjs to resolve.extensions', () => {
273
+ const context = new Context('/virtual');
274
+ context.addFile(WEBPACK_CONFIG_PATH, webpackConfigWithResolve);
275
+
276
+ const result = bundleGrafanaUI(context, {});
277
+
278
+ const content = result.getFile(WEBPACK_CONFIG_PATH) || '';
279
+ expect(content).toMatch(
280
+ /extensions:\s*\[['"]\.js['"],\s*['"]\.jsx['"],\s*['"]\.ts['"],\s*['"]\.tsx['"],\s*['"]\.mjs['"]/
281
+ );
282
+ });
283
+
284
+ it('should add module rule for .mjs files with resolve.fullySpecified: false', () => {
285
+ const context = new Context('/virtual');
286
+ context.addFile(WEBPACK_CONFIG_PATH, webpackConfigWithResolve);
287
+
288
+ const result = bundleGrafanaUI(context, {});
289
+
290
+ const content = result.getFile(WEBPACK_CONFIG_PATH) || '';
291
+ // Check for module rule with .mjs test, node_modules include, and resolve.fullySpecified: false
292
+ expect(content).toMatch(/test:\s*\/\\\.mjs\$/);
293
+ expect(content).toMatch(/include:\s*\/node_modules\//);
294
+ expect(content).toMatch(/resolve:\s*\{[^}]*fullySpecified:\s*false/);
295
+ });
296
+
297
+ it('should insert .mjs rule at position 1 (after imports-loader rule)', () => {
298
+ const webpackConfigWithImportsLoader = `import type { Configuration } from 'webpack';
299
+
300
+ const baseConfig: Configuration = {
301
+ module: {
302
+ rules: [
303
+ {
304
+ test: /src\\/(?:.*\\/)?module\\.tsx?$/,
305
+ use: [
306
+ {
307
+ loader: 'imports-loader',
308
+ options: {
309
+ imports: 'side-effects grafana-public-path',
310
+ },
311
+ },
312
+ ],
313
+ },
314
+ ],
315
+ },
316
+ resolve: {
317
+ extensions: ['.js', '.jsx', '.ts', '.tsx'],
318
+ modules: ['node_modules'],
319
+ },
320
+ };
321
+
322
+ export default baseConfig;`;
323
+
324
+ const context = new Context('/virtual');
325
+ context.addFile(WEBPACK_CONFIG_PATH, webpackConfigWithImportsLoader);
326
+
327
+ const result = bundleGrafanaUI(context, {});
328
+
329
+ const content = result.getFile(WEBPACK_CONFIG_PATH) || '';
330
+ // Find the position of imports-loader rule and .mjs rule
331
+ const importsLoaderIndex = content.indexOf('imports-loader');
332
+ const mjsRuleIndex = content.indexOf('test: /\\.mjs$/');
333
+
334
+ // .mjs rule should come after imports-loader rule
335
+ expect(mjsRuleIndex).toBeGreaterThan(importsLoaderIndex);
336
+
337
+ // Verify .mjs rule is present
338
+ expect(content).toMatch(/test:\s*\/\\\.mjs\$/);
339
+ });
340
+
341
+ it('should be idempotent for resolve configuration', () => {
342
+ const context = new Context('/virtual');
343
+ context.addFile(WEBPACK_CONFIG_PATH, webpackConfigWithResolve);
344
+
345
+ const result1 = bundleGrafanaUI(context, {});
346
+ const content1 = result1.getFile(WEBPACK_CONFIG_PATH) || '';
347
+
348
+ // Verify first run added .mjs to extensions and module rule
349
+ expect(content1).toMatch(/['"]\.mjs['"]/);
350
+ expect(content1).toMatch(/test:\s*\/\\\.mjs\$/);
351
+ expect(content1).toMatch(/resolve:\s*\{[^}]*fullySpecified:\s*false/);
352
+
353
+ const context2 = new Context('/virtual');
354
+ context2.addFile(WEBPACK_CONFIG_PATH, content1);
355
+ const result2 = bundleGrafanaUI(context2, {});
356
+
357
+ // Second run should produce identical content (idempotent)
358
+ const content2 = result2.getFile(WEBPACK_CONFIG_PATH) || '';
359
+ expect(content2).toBe(content1);
360
+ });
361
+
362
+ it('should not duplicate .mjs if already present', () => {
363
+ const webpackConfigWithMjs = `import type { Configuration } from 'webpack';
364
+
365
+ const baseConfig: Configuration = {
366
+ resolve: {
367
+ extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs'],
368
+ modules: ['node_modules'],
369
+ },
370
+ };
371
+
372
+ export default baseConfig;`;
373
+
374
+ const context = new Context('/virtual');
375
+ context.addFile(WEBPACK_CONFIG_PATH, webpackConfigWithMjs);
376
+
377
+ const result = bundleGrafanaUI(context, {});
378
+
379
+ const content = result.getFile(WEBPACK_CONFIG_PATH) || '';
380
+ // Count occurrences of .mjs in extensions array
381
+ const mjsMatches = content.match(/['"]\.mjs['"]/g);
382
+ expect(mjsMatches?.length).toBe(1);
383
+ });
384
+
385
+ it('should not duplicate .mjs module rule if already present', () => {
386
+ const webpackConfigWithMjsRule = `import type { Configuration } from 'webpack';
387
+
388
+ const baseConfig: Configuration = {
389
+ module: {
390
+ rules: [
391
+ {
392
+ test: /\\.mjs$/,
393
+ include: /node_modules/,
394
+ resolve: {
395
+ fullySpecified: false,
396
+ },
397
+ type: 'javascript/auto',
398
+ },
399
+ ],
400
+ },
401
+ resolve: {
402
+ extensions: ['.js', '.jsx', '.ts', '.tsx'],
403
+ modules: ['node_modules'],
404
+ },
405
+ };
406
+
407
+ export default baseConfig;`;
408
+
409
+ const context = new Context('/virtual');
410
+ context.addFile(WEBPACK_CONFIG_PATH, webpackConfigWithMjsRule);
411
+
412
+ const result = bundleGrafanaUI(context, {});
413
+
414
+ const content = result.getFile(WEBPACK_CONFIG_PATH) || '';
415
+ // Count occurrences of .mjs test pattern in module rules
416
+ const mjsRuleMatches = content.match(/test:\s*\/\\\.mjs\$/g);
417
+ expect(mjsRuleMatches?.length).toBe(1);
418
+ });
419
+ });
420
+
421
+ describe('rspack.config.ts', () => {
422
+ it('should add .mjs to resolve.extensions', () => {
423
+ const context = new Context('/virtual');
424
+ context.addFile(RSPACK_CONFIG_PATH, rspackConfigWithResolve);
425
+
426
+ const result = bundleGrafanaUI(context, {});
427
+
428
+ const content = result.getFile(RSPACK_CONFIG_PATH) || '';
429
+ expect(content).toMatch(
430
+ /extensions:\s*\[['"]\.js['"],\s*['"]\.jsx['"],\s*['"]\.ts['"],\s*['"]\.tsx['"],\s*['"]\.mjs['"]/
431
+ );
432
+ });
433
+
434
+ it('should add module rule for .mjs files with resolve.fullySpecified: false', () => {
435
+ const context = new Context('/virtual');
436
+ context.addFile(RSPACK_CONFIG_PATH, rspackConfigWithResolve);
437
+
438
+ const result = bundleGrafanaUI(context, {});
439
+
440
+ const content = result.getFile(RSPACK_CONFIG_PATH) || '';
441
+ // Check for module rule with .mjs test, node_modules include, and resolve.fullySpecified: false
442
+ expect(content).toMatch(/test:\s*\/\\\.mjs\$/);
443
+ expect(content).toMatch(/include:\s*\/node_modules\//);
444
+ expect(content).toMatch(/resolve:\s*\{[^}]*fullySpecified:\s*false/);
445
+ });
446
+
447
+ it('should insert .mjs rule at position 1 (after imports-loader rule)', () => {
448
+ const rspackConfigWithImportsLoader = `import type { Configuration } from '@rspack/core';
449
+
450
+ const baseConfig: Configuration = {
451
+ module: {
452
+ rules: [
453
+ {
454
+ test: /src\\/(?:.*\\/)?module\\.tsx?$/,
455
+ use: [
456
+ {
457
+ loader: 'imports-loader',
458
+ options: {
459
+ imports: 'side-effects grafana-public-path',
460
+ },
461
+ },
462
+ ],
463
+ },
464
+ ],
465
+ },
466
+ resolve: {
467
+ extensions: ['.js', '.jsx', '.ts', '.tsx'],
468
+ modules: ['node_modules'],
469
+ },
470
+ };
471
+
472
+ export default baseConfig;`;
473
+
474
+ const context = new Context('/virtual');
475
+ context.addFile(RSPACK_CONFIG_PATH, rspackConfigWithImportsLoader);
476
+
477
+ const result = bundleGrafanaUI(context, {});
478
+
479
+ const content = result.getFile(RSPACK_CONFIG_PATH) || '';
480
+ // Find the position of imports-loader rule and .mjs rule
481
+ const importsLoaderIndex = content.indexOf('imports-loader');
482
+ const mjsRuleIndex = content.indexOf('test: /\\.mjs$/');
483
+
484
+ // .mjs rule should come after imports-loader rule
485
+ expect(mjsRuleIndex).toBeGreaterThan(importsLoaderIndex);
486
+
487
+ // Verify .mjs rule is present
488
+ expect(content).toMatch(/test:\s*\/\\\.mjs\$/);
489
+ });
490
+
491
+ it('should prefer rspack.config.ts over webpack.config.ts when both exist', () => {
492
+ const context = new Context('/virtual');
493
+ context.addFile(RSPACK_CONFIG_PATH, rspackConfigWithResolve);
494
+ context.addFile(WEBPACK_CONFIG_PATH, webpackConfigWithResolve);
495
+
496
+ const result = bundleGrafanaUI(context, {});
497
+
498
+ // rspack.config.ts should be updated
499
+ const rspackContent = result.getFile(RSPACK_CONFIG_PATH) || '';
500
+ expect(rspackContent).toMatch(/['"]\.mjs['"]/);
501
+ expect(rspackContent).toMatch(/test:\s*\/\\\.mjs\$/);
502
+ expect(rspackContent).toMatch(/resolve:\s*\{[^}]*fullySpecified:\s*false/);
503
+
504
+ // webpack.config.ts should NOT be updated
505
+ const webpackContent = result.getFile(WEBPACK_CONFIG_PATH) || '';
506
+ expect(webpackContent).not.toMatch(/['"]\.mjs['"]/);
507
+ expect(webpackContent).not.toMatch(/test:\s*\/\\\.mjs\$/);
508
+ });
509
+ });
510
+ });
511
+ });